diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6df27b4876..cc97bc569c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,20 +4,20 @@ ## Feature requests -If you are requesting a new feature, please check out the list of [often requested features](https://firefly-iii.github.io/requested-features/). +I am always interested in expanding Firefly III's many features. If you are requesting a new feature, please check out the list of [often requested features](https://firefly-iii.github.io/requested-features/). ## Bugs -If you find a bug, please take the time and see if the [demo site](https://firefly-iii.nder.be/) is also suffering from this bug. Include as many log files and details as you think are necessary. +First of all: thank you for reporting a bug instead of ditching the tool altogether. If you find a bug, please take the time and see if the [demo site](https://firefly-iii.nder.be/) is also suffering from this bug. Include as many log files and details as you think are necessary. Bugs have a lot of priority! ## Installation problems -Take the time to read the [installation guide FAQ](https://firefly-iii.github.io/installation-guide-faq/) and make sure you search through closed issues for the problems other people have had. Your problem may be among them! +Please take the time to read the [installation guide FAQ](https://firefly-iii.github.io/installation-guide-faq/) and make sure you search through closed issues for the problems other people have had. Your problem may be among them! If not, open an issue and I will help where I can. ## Pull requests -I can only accept pull requests against the `develop` branch, never the `master` branch. +When contributing to Firefly III, please first discuss the change you wish to make via issue, email, or any other method. I can only accept pull requests against the `develop` branch, never the `master` branch. ## Translations :us: :fr: :de: -If you see a spelling error, grammatical error or a weird translation in your language, please join [our CrowdIn](https://crowdin.com/project/firefly-iii) project. There, you can submit your translations and fixes. The GitHub repository will download these automatically and they will be included in the next release. \ No newline at end of file +If you see a spelling error, grammatical error or a weird translation in your language, please join [our CrowdIn](https://crowdin.com/project/firefly-iii) project. There, you can submit your translations and fixes. The GitHub repository will download these automatically and they will be included in the next release. diff --git a/.gitignore b/.gitignore index b1e27bfc74..d4cfc0d4fb 100755 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Homestead.json Homestead.yaml .env public/google*.html +report.html diff --git a/.travis.yml b/.travis.yml index 9e633c9bb1..930e42cb60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ install: - php artisan optimize - php artisan env - cp .env.testing .env - - mv storage/database/databasecopy.sqlite storage/database/database.sqlite + - wget -q https://github.com/firefly-iii/test-data/raw/master/storage/database.sqlite -O storage/database/database.sqlite - mkdir -p build/logs script: diff --git a/CHANGELOG.md b/CHANGELOG.md index a64c684432..f68ff4250a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,44 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [4.6.2] - 2017-07-08 +### Added +- Links added to boxes, idea by @simonsmiley + +### Fixed +- Various bugs in import routine + +## [4.6.1] - 2017-07-02 +### Fixed +- Fixed several small issues all around. + +## [4.6.0] - 2017-06-28 + +### Changed +- Revamped import routine. Will be buggy. + +### Fixed +- Issue #667, postgresql reported by @skibbipl. +- Issue #680 by @Xeli +- Fixed #660 +- Fixes #672, reported by @dzaikos +- Translation error fixed by +- Fix a bug where the balance routine forgot to account for accounts without a currency preference. +- Various other bugfixes. + +## [4.5.0] - 2017-06-07 + +### Added +- Better support for multi-currency transactions and display of transactions, accounts and everything. This requires a database overhaul (moving the currency information to specific transactions) so be careful when upgrading. +- Translations for Spanish and Slovenian. +- New interface for budget page, ~~stolen from~~ inspired by YNAB. +- Expanded Docker to work with postgresql as well, thanks to @kressh + +### Fixed +- PostgreSQL support in database upgrade routine (#644, reported by @skibbipl) +- Frontpage budget chart was off, fix by @nhaarman +- Was not possible to remove opening balance. + ## [4.4.3] - 2017-05-03 ### Added - Added support for Slovenian diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..9e6b350466 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at thegrumpydictator@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Dockerfile b/Dockerfile index 7d00a3724b..794634fdb8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,13 +11,14 @@ RUN apt-get update -y && \ libtidy-dev \ libxml2-dev \ libsqlite3-dev \ + libpq-dev \ libbz2-dev \ gettext-base \ locales && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* -RUN docker-php-ext-install -j$(nproc) curl gd intl json mcrypt readline tidy zip bcmath xml mbstring pdo_sqlite pdo_mysql bz2 +RUN docker-php-ext-install -j$(nproc) curl gd intl json mcrypt readline tidy zip bcmath xml mbstring pdo_sqlite pdo_mysql bz2 pdo_pgsql # Generate locales supported by firefly RUN echo "en_US.UTF-8 UTF-8\nde_DE.UTF-8 UTF-8\nnl_NL.UTF-8 UTF-8\npt_BR.UTF-8 UTF-8" > /etc/locale.gen && locale-gen diff --git a/README.md b/README.md index 069c7cbee8..92467107fd 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,9 @@ [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/firefly-iii/firefly-iii/tree/master) -Firefly III can be run on Heroku. Register for a free Heroku account and instantly run Firefly III on your very own cloud instance. +Firefly III can be run on Heroku. Register for a free Heroku account and instantly run Firefly III on your very own cloud instance. There is also a [demo site](https://firefly-iii.nder.be) with an example financial administration already present. -There is also a [demo site](https://firefly-iii.nder.be) with an example financial administration already present. - -## Installation +## Getting started To install Firefly III, you'll need a web server (preferrably on Linux) and access to the command line. Then, please read the [installation guide](https://firefly-iii.github.io/using-installing.html). @@ -26,7 +24,7 @@ Personal financial management is pretty difficult, and everybody has their own a Firefly works on the principle that if you know where you're money is going, you can stop it from going there. -#### Some advantages of using Firefly +### Some advantages of using Firefly - Firefly can import any CSV file, so migrating from other systems is easy. - Firefly runs on your own server, so you are fully in control of your data. Remember, there is no such thing as "the cloud", it’s just somebody else’s computer! @@ -35,6 +33,25 @@ Firefly works on the principle that if you know where you're money is going, you Firefly is pretty awesome. [You can read more about Firefly III, and its features, on the Github Pages](https://firefly-iii.github.io/). +### Contributing + +Please read [CONTRIBUTING.md](https://github.com/firefly-iii/firefly-iii/blob/master/.github/CONTRIBUTING.md) for details on contributing, and the process for submitting pull requests. Please check out the [code of conduct](https://github.com/firefly-iii/firefly-iii/blob/master/CODE_OF_CONDUCT.md) as well. + +### Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see [the tags](https://github.com/firefly-iii/firefly-iii/tags) on this repository. + +### Authors + +* James Cole +* Over time, [many people have contributed to Firefly III](https://github.com/firefly-iii/firefly-iii/graphs/contributors). + +### License + +This work [is licensed](https://github.com/firefly-iii/firefly-iii/blob/master/LICENSE) under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/). + +### Other stuff + If you like Firefly and if it helps you save lots of money, why not send me [a dime for every dollar saved](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=44UKUT455HUFA) (this is a joke, although the Paypal form works just fine, try it!) If you want to contact me, please open an issue or [email me](mailto:thegrumpydictator@gmail.com). diff --git a/app/Console/Commands/CreateImport.php b/app/Console/Commands/CreateImport.php index 3c0d14fb18..adec1162db 100644 --- a/app/Console/Commands/CreateImport.php +++ b/app/Console/Commands/CreateImport.php @@ -14,10 +14,14 @@ declare(strict_types=1); namespace FireflyIII\Console\Commands; use Artisan; +use FireflyIII\Import\Logging\CommandHandler; +use FireflyIII\Import\Routine\ImportRoutine; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Repositories\User\UserRepositoryInterface; use Illuminate\Console\Command; +use Illuminate\Support\MessageBag; use Log; +use Monolog\Formatter\LineFormatter; /** * Class CreateImport @@ -73,30 +77,55 @@ class CreateImport extends Command return; } - $this->info(sprintf('Going to create a job to import file: %s', $file)); - $this->info(sprintf('Using configuration file: %s', $configuration)); - $this->info(sprintf('Import into user: #%d (%s)', $user->id, $user->email)); - $this->info(sprintf('Type of import: %s', $type)); + $this->line(sprintf('Going to create a job to import file: %s', $file)); + $this->line(sprintf('Using configuration file: %s', $configuration)); + $this->line(sprintf('Import into user: #%d (%s)', $user->id, $user->email)); + $this->line(sprintf('Type of import: %s', $type)); + /** @var ImportJobRepositoryInterface $jobRepository */ $jobRepository = app(ImportJobRepositoryInterface::class); $jobRepository->setUser($user); $job = $jobRepository->create($type); - $this->line(sprintf('Created job "%s"...', $job->key)); + $this->line(sprintf('Created job "%s"', $job->key)); + Artisan::call('firefly:encrypt-file', ['file' => $file, 'key' => $job->key]); $this->line('Stored import data...'); + $job->configuration = $configurationData; - $job->status = 'settings_complete'; + $job->status = 'configured'; $job->save(); $this->line('Stored configuration...'); + if ($this->option('start') === true) { $this->line('The import will start in a moment. This process is not visible...'); Log::debug('Go for import!'); - Artisan::call('firefly:start-import', ['key' => $job->key]); - $this->line('Done!'); + + // normally would refer to other firefly:start-import but that doesn't seem to work all to well... + $monolog = Log::getMonolog(); + $handler = new CommandHandler($this); + $formatter = new LineFormatter(null, null, false, true); + $handler->setFormatter($formatter); + $monolog->pushHandler($handler); + + + // start the actual routine: + /** @var ImportRoutine $routine */ + $routine = app(ImportRoutine::class); + $routine->setJob($job); + $routine->run(); + + // give feedback. + /** @var MessageBag $error */ + foreach ($routine->errors as $index => $error) { + $this->error(sprintf('Error importing line #%d: %s', $index, $error)); + } + $this->line( + sprintf('The import has finished. %d transactions have been imported out of %d records.', $routine->journals->count(), $routine->lines) + ); } return; diff --git a/app/Console/Commands/Import.php b/app/Console/Commands/Import.php index d2ff9b8997..f37203cc41 100644 --- a/app/Console/Commands/Import.php +++ b/app/Console/Commands/Import.php @@ -13,12 +13,11 @@ declare(strict_types=1); namespace FireflyIII\Console\Commands; -use FireflyIII\Import\ImportProcedure; use FireflyIII\Import\Logging\CommandHandler; +use FireflyIII\Import\Routine\ImportRoutine; use FireflyIII\Models\ImportJob; -use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; -use Illuminate\Support\Collection; +use Illuminate\Support\MessageBag; use Log; /** @@ -75,15 +74,18 @@ class Import extends Command $monolog = Log::getMonolog(); $handler = new CommandHandler($this); $monolog->pushHandler($handler); - $importProcedure = new ImportProcedure; - $result = $importProcedure->runImport($job); - // display result to user: - $this->presentResults($result); - $this->line('The import has completed.'); + /** @var ImportRoutine $routine */ + $routine = app(ImportRoutine::class); + $routine->setJob($job); + $routine->run(); - // get any errors from the importer: - $this->presentErrors($job); + /** @var MessageBag $error */ + foreach ($routine->errors as $index => $error) { + $this->error(sprintf('Error importing line #%d: %s', $index, $error)); + } + + $this->line(sprintf('The import has finished. %d transactions have been imported out of %d records.', $routine->journals->count(), $routine->lines)); return; } @@ -96,12 +98,14 @@ class Import extends Command private function isValid(ImportJob $job): bool { if (is_null($job)) { + Log::error('This job does not seem to exist.'); $this->error('This job does not seem to exist.'); return false; } - if ($job->status != 'settings_complete') { + if ($job->status !== 'configured') { + Log::error(sprintf('This job is not ready to be imported (status is %s).', $job->status)); $this->error('This job is not ready to be imported.'); return false; @@ -109,36 +113,4 @@ class Import extends Command return true; } - - /** - * @param ImportJob $job - */ - private function presentErrors(ImportJob $job) - { - $extendedStatus = $job->extended_status; - if (isset($extendedStatus['errors']) && count($extendedStatus['errors']) > 0) { - $this->line(sprintf('The following %d error(s) occured during the import:', count($extendedStatus['errors']))); - foreach ($extendedStatus['errors'] as $error) { - $this->error($error); - } - } - } - - /** - * @param Collection $result - */ - private function presentResults(Collection $result) - { - /** - * @var int $index - * @var TransactionJournal $journal - */ - foreach ($result as $index => $journal) { - if (!is_null($journal->id)) { - $this->line(sprintf('Line #%d has been imported as transaction #%d.', $index, $journal->id)); - continue; - } - $this->error(sprintf('Could not store line #%d', $index)); - } - } } diff --git a/app/Console/Commands/UpgradeDatabase.php b/app/Console/Commands/UpgradeDatabase.php index d313c55ec6..42566a6cee 100644 --- a/app/Console/Commands/UpgradeDatabase.php +++ b/app/Console/Commands/UpgradeDatabase.php @@ -32,6 +32,7 @@ use Illuminate\Database\QueryException; use Log; use Preferences; use Schema; +use Steam; /** * Class UpgradeDatabase @@ -72,9 +73,54 @@ class UpgradeDatabase extends Command $this->repairPiggyBanks(); $this->updateAccountCurrencies(); $this->updateJournalCurrencies(); + $this->currencyInfoToTransactions(); + $this->verifyCurrencyInfo(); $this->info('Firefly III database is up to date.'); } + /** + * Moves the currency id info to the transaction instead of the journal. + */ + private function currencyInfoToTransactions() + { + $count = 0; + $set = TransactionJournal::with('transactions')->get(); + /** @var TransactionJournal $journal */ + foreach ($set as $journal) { + /** @var Transaction $transaction */ + foreach ($journal->transactions as $transaction) { + if (is_null($transaction->transaction_currency_id)) { + $transaction->transaction_currency_id = $journal->transaction_currency_id; + $transaction->save(); + $count++; + } + } + + + // read and use the foreign amounts when present. + if ($journal->hasMeta('foreign_amount')) { + $amount = Steam::positive($journal->getMeta('foreign_amount')); + + // update both transactions: + foreach ($journal->transactions as $transaction) { + $transaction->foreign_amount = $amount; + if (bccomp($transaction->amount, '0') === -1) { + // update with negative amount: + $transaction->foreign_amount = bcmul($amount, '-1'); + } + // set foreign currency id: + $transaction->foreign_currency_id = intval($journal->getMeta('foreign_currency_id')); + $transaction->save(); + } + $journal->deleteMeta('foreign_amount'); + $journal->deleteMeta('foreign_currency_id'); + } + + } + + $this->line(sprintf('Updated currency information for %d transactions', $count)); + } + /** * Migrate budget repetitions to new format. */ @@ -269,9 +315,11 @@ class UpgradeDatabase extends Command $repository = app(CurrencyRepositoryInterface::class); $notification = '%s #%d uses %s but should use %s. It has been updated. Please verify this in Firefly III.'; $transfer = 'Transfer #%d has been updated to use the correct currencies. Please verify this in Firefly III.'; + $driver = DB::connection()->getDriverName(); + $pgsql = ['pgsql', 'postgresql']; foreach ($types as $type => $operator) { - $set = TransactionJournal + $query = TransactionJournal ::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')->leftJoin( 'transactions', function (JoinClause $join) use ($operator) { $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', $operator, '0'); @@ -280,9 +328,15 @@ class UpgradeDatabase extends Command ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id') ->where('transaction_types.type', $type) - ->where('account_meta.name', 'currency_id') - ->where('transaction_journals.transaction_currency_id', '!=', DB::raw('account_meta.data')) - ->get(['transaction_journals.*', 'account_meta.data as expected_currency_id', 'transactions.amount as transaction_amount']); + ->where('account_meta.name', 'currency_id'); + if (in_array($driver, $pgsql)) { + $query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('cast(account_meta.data as int)')); + } + if (!in_array($driver, $pgsql)) { + $query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('account_meta.data')); + } + + $set = $query->get(['transaction_journals.*', 'account_meta.data as expected_currency_id', 'transactions.amount as transaction_amount']); /** @var TransactionJournal $journal */ foreach ($set as $journal) { $expectedCurrency = $repository->find(intval($journal->expected_currency_id)); @@ -334,4 +388,25 @@ class UpgradeDatabase extends Command } } } + + /** + * + */ + private function verifyCurrencyInfo() + { + $count = 0; + $transactions = Transaction::get(); + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $currencyId = intval($transaction->transaction_currency_id); + $foreignId = intval($transaction->foreign_currency_id); + if ($currencyId === $foreignId) { + $transaction->foreign_currency_id = null; + $transaction->foreign_amount = null; + $transaction->save(); + $count++; + } + } + $this->line(sprintf('Updated currency information for %d transactions', $count)); + } } diff --git a/app/Console/Commands/UpgradeFireflyInstructions.php b/app/Console/Commands/UpgradeFireflyInstructions.php index f98e84f86a..363e5089f7 100644 --- a/app/Console/Commands/UpgradeFireflyInstructions.php +++ b/app/Console/Commands/UpgradeFireflyInstructions.php @@ -50,10 +50,10 @@ class UpgradeFireflyInstructions extends Command public function handle() { - if ($this->argument('task') == 'update') { + if ($this->argument('task') === 'update') { $this->updateInstructions(); } - if ($this->argument('task') == 'install') { + if ($this->argument('task') === 'install') { $this->installInstructions(); } } diff --git a/app/Console/Commands/VerifyDatabase.php b/app/Console/Commands/VerifyDatabase.php index 52cf80c265..6e128e7f53 100644 --- a/app/Console/Commands/VerifyDatabase.php +++ b/app/Console/Commands/VerifyDatabase.php @@ -259,7 +259,7 @@ class VerifyDatabase extends Command { $plural = str_plural($name); $class = sprintf('FireflyIII\Models\%s', ucfirst($name)); - $field = $name == 'tag' ? 'tag' : 'name'; + $field = $name === 'tag' ? 'tag' : 'name'; $set = $class::leftJoin($name . '_transaction_journal', $plural . '.id', '=', $name . '_transaction_journal.' . $name . '_id') ->leftJoin('users', $plural . '.user_id', '=', 'users.id') ->distinct() diff --git a/app/Events/StoredTransactionJournal.php b/app/Events/StoredTransactionJournal.php index d1d6805cd2..e67cfbe2cc 100644 --- a/app/Events/StoredTransactionJournal.php +++ b/app/Events/StoredTransactionJournal.php @@ -26,9 +26,9 @@ class StoredTransactionJournal extends Event use SerializesModels; - /** @var TransactionJournal */ + /** @var TransactionJournal */ public $journal; - /** @var int */ + /** @var int */ public $piggyBankId; /** diff --git a/app/Events/UpdatedTransactionJournal.php b/app/Events/UpdatedTransactionJournal.php index ce21810bc2..2390a13d75 100644 --- a/app/Events/UpdatedTransactionJournal.php +++ b/app/Events/UpdatedTransactionJournal.php @@ -26,7 +26,7 @@ class UpdatedTransactionJournal extends Event use SerializesModels; - /** @var TransactionJournal */ + /** @var TransactionJournal */ public $journal; /** diff --git a/app/Export/Collector/JournalExportCollector.php b/app/Export/Collector/JournalExportCollector.php index 175b405f79..b0e04e115e 100644 --- a/app/Export/Collector/JournalExportCollector.php +++ b/app/Export/Collector/JournalExportCollector.php @@ -303,7 +303,7 @@ class JournalExportCollector extends BasicCollector implements CollectorInterfac ->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id') ->leftJoin('accounts AS opposing_accounts', 'opposing.account_id', '=', 'opposing_accounts.id') ->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', 'transaction_types.id') - ->leftJoin('transaction_currencies', 'transaction_journals.transaction_currency_id', '=', 'transaction_currencies.id') + ->leftJoin('transaction_currencies', 'transactions.transaction_currency_id', '=', 'transaction_currencies.id') ->whereIn('transactions.account_id', $accountIds) ->where('transaction_journals.user_id', $this->job->user_id) ->where('transaction_journals.date', '>=', $this->start->format('Y-m-d')) @@ -338,7 +338,7 @@ class JournalExportCollector extends BasicCollector implements CollectorInterfac 'transaction_journals.encrypted as journal_encrypted', 'transaction_journals.transaction_type_id', 'transaction_types.type as transaction_type', - 'transaction_journals.transaction_currency_id', + 'transactions.transaction_currency_id', 'transaction_currencies.code AS transaction_currency_code', ] diff --git a/app/Export/Entry/Entry.php b/app/Export/Entry/Entry.php index f4061f703b..13be79458e 100644 --- a/app/Export/Entry/Entry.php +++ b/app/Export/Entry/Entry.php @@ -88,7 +88,7 @@ final class Entry $entry->budget_name = $object->budget_name ?? ''; // update description when transaction description is different: - if (!is_null($object->description) && $object->description != $entry->description) { + if (!is_null($object->description) && $object->description !== $entry->description) { $entry->description = $entry->description . ' (' . $object->description . ')'; } diff --git a/app/Generator/Chart/Basic/ChartJsGenerator.php b/app/Generator/Chart/Basic/ChartJsGenerator.php index b63f9e9c01..b8086880f4 100644 --- a/app/Generator/Chart/Basic/ChartJsGenerator.php +++ b/app/Generator/Chart/Basic/ChartJsGenerator.php @@ -110,9 +110,16 @@ class ChartJsGenerator implements GeneratorInterface ]; // sort by value, keep keys. + // different sort when values are positive and when they're negative. asort($data); + $next = next($data); + if (!is_bool($next) && bccomp($next, '0') === 1) { + // next is positive, sort other way around. + arsort($data); + } + unset($next); - $index = 0; + $index = 0; foreach ($data as $key => $value) { // make larger than 0 diff --git a/app/Generator/Chart/Basic/GeneratorInterface.php b/app/Generator/Chart/Basic/GeneratorInterface.php index 92b1b29d93..31deeb0a30 100644 --- a/app/Generator/Chart/Basic/GeneratorInterface.php +++ b/app/Generator/Chart/Basic/GeneratorInterface.php @@ -22,10 +22,15 @@ interface GeneratorInterface { /** - * Will generate a (ChartJS) compatible array from the given input. Expects this format: + * Will generate a Chart JS compatible array from the given input. Expects this format + * + * Will take labels for all from first set. * * 0: [ * 'label' => 'label of set', + * 'type' => bar or line, optional + * 'yAxisID' => ID of yAxis, optional, will not be included when unused. + * 'fill' => if to fill a line? optional, will not be included when unused. * 'entries' => * [ * 'label-of-entry' => 'value' @@ -33,12 +38,16 @@ interface GeneratorInterface * ] * 1: [ * 'label' => 'label of another set', + * 'type' => bar or line, optional + * 'yAxisID' => ID of yAxis, optional, will not be included when unused. + * 'fill' => if to fill a line? optional, will not be included when unused. * 'entries' => * [ * 'label-of-entry' => 'value' * ] * ] * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's five. * * @param array $data * diff --git a/app/Generator/Report/Audit/MonthReportGenerator.php b/app/Generator/Report/Audit/MonthReportGenerator.php index 0b12e60fe0..43e23ad1a4 100644 --- a/app/Generator/Report/Audit/MonthReportGenerator.php +++ b/app/Generator/Report/Audit/MonthReportGenerator.php @@ -19,6 +19,7 @@ use FireflyIII\Generator\Report\ReportGeneratorInterface; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use Illuminate\Support\Collection; use Steam; @@ -147,6 +148,8 @@ class MonthReportGenerator implements ReportGeneratorInterface */ private function getAuditReport(Account $account, Carbon $date): array { + /** @var CurrencyRepositoryInterface $currencyRepos */ + $currencyRepos = app(CurrencyRepositoryInterface::class); /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class); @@ -155,15 +158,21 @@ class MonthReportGenerator implements ReportGeneratorInterface $journals = $journals->reverse(); $dayBeforeBalance = Steam::balance($account, $date); $startBalance = $dayBeforeBalance; - + $currency = $currencyRepos->find(intval($account->getMeta('currency_id'))); /** @var Transaction $journal */ foreach ($journals as $transaction) { $transaction->before = $startBalance; $transactionAmount = $transaction->transaction_amount; - $newBalance = bcadd($startBalance, $transactionAmount); - $transaction->after = $newBalance; - $startBalance = $newBalance; + + if ($currency->id === $transaction->foreign_currency_id) { + $transactionAmount = $transaction->transaction_foreign_amount; + } + + $newBalance = bcadd($startBalance, $transactionAmount); + $transaction->after = $newBalance; + $startBalance = $newBalance; + $transaction->currency = $currency; } /* diff --git a/app/Generator/Report/Support.php b/app/Generator/Report/Support.php index 563e72f12b..674b31f7d6 100644 --- a/app/Generator/Report/Support.php +++ b/app/Generator/Report/Support.php @@ -15,7 +15,6 @@ namespace FireflyIII\Generator\Report; use FireflyIII\Models\Transaction; use Illuminate\Support\Collection; -use Log; /** diff --git a/app/Generator/Report/Tag/MonthReportGenerator.php b/app/Generator/Report/Tag/MonthReportGenerator.php index f8a253defd..3f247f4467 100644 --- a/app/Generator/Report/Tag/MonthReportGenerator.php +++ b/app/Generator/Report/Tag/MonthReportGenerator.php @@ -14,6 +14,7 @@ namespace FireflyIII\Generator\Report\Tag; use Carbon\Carbon; use FireflyIII\Generator\Report\ReportGeneratorInterface; +use FireflyIII\Generator\Report\Support; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Helpers\Filter\NegativeAmountFilter; use FireflyIII\Helpers\Filter\OpposingAccountFilter; @@ -30,7 +31,7 @@ use Log; * * @package FireflyIII\Generator\Report\Tag */ -class MonthReportGenerator implements ReportGeneratorInterface +class MonthReportGenerator extends Support implements ReportGeneratorInterface { /** @var Collection */ diff --git a/app/Handlers/Events/StoredJournalEventHandler.php b/app/Handlers/Events/StoredJournalEventHandler.php index 7a880fc699..6d266d0dda 100644 --- a/app/Handlers/Events/StoredJournalEventHandler.php +++ b/app/Handlers/Events/StoredJournalEventHandler.php @@ -42,6 +42,10 @@ class StoredJournalEventHandler /** * StoredJournalEventHandler constructor. + * + * @param PRI $repository + * @param JRI $journalRepository + * @param RGRI $ruleGroupRepository */ public function __construct(PRI $repository, JRI $journalRepository, RGRI $ruleGroupRepository) { diff --git a/app/Handlers/Events/UpdatedJournalEventHandler.php b/app/Handlers/Events/UpdatedJournalEventHandler.php index 1cdadd6da8..17303c9134 100644 --- a/app/Handlers/Events/UpdatedJournalEventHandler.php +++ b/app/Handlers/Events/UpdatedJournalEventHandler.php @@ -23,7 +23,7 @@ use FireflyIII\Support\Events\BillScanner; /** * @codeCoverageIgnore - * + * * Class UpdatedJournalEventHandler * * @package FireflyIII\Handlers\Events @@ -35,6 +35,8 @@ class UpdatedJournalEventHandler /** * StoredJournalEventHandler constructor. + * + * @param RuleGroupRepositoryInterface $ruleGroupRepository */ public function __construct(RuleGroupRepositoryInterface $ruleGroupRepository) { @@ -52,7 +54,7 @@ class UpdatedJournalEventHandler { // get all the user's rule groups, with the rules, order by 'order'. $journal = $updatedJournalEvent->journal; - $groups = $this->repository->getActiveGroups($journal->user); + $groups = $this->repository->getActiveGroups($journal->user); /** @var RuleGroup $group */ foreach ($groups as $group) { diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index 4b02a2a455..8c7eb4a90c 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -74,6 +74,7 @@ class UserEventHandler } catch (Swift_TransportException $e) { Log::error($e->getMessage()); } + // @codeCoverageIgnoreEnd return true; @@ -96,16 +97,17 @@ class UserEventHandler } // get the email address $email = $event->user->email; - $address = route('index'); + $uri = route('index'); $ipAddress = $event->ipAddress; // send email. try { - Mail::to($email)->send(new RegisteredUserMail($address, $ipAddress)); + Mail::to($email)->send(new RegisteredUserMail($uri, $ipAddress)); // @codeCoverageIgnoreStart } catch (Swift_TransportException $e) { Log::error($e->getMessage()); } + // @codeCoverageIgnoreEnd return true; diff --git a/app/Helpers/Attachments/AttachmentHelper.php b/app/Helpers/Attachments/AttachmentHelper.php index 3189055bb2..af83f729a7 100644 --- a/app/Helpers/Attachments/AttachmentHelper.php +++ b/app/Helpers/Attachments/AttachmentHelper.php @@ -18,9 +18,10 @@ use FireflyIII\Models\Attachment; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\MessageBag; +use Log; use Storage; use Symfony\Component\HttpFoundation\File\UploadedFile; -use Log; + /** * Class AttachmentHelper * @@ -157,11 +158,14 @@ class AttachmentHelper implements AttachmentHelperInterface $attachment->size = $file->getSize(); $attachment->uploaded = 0; $attachment->save(); + Log::debug('Created attachment:', $attachment->toArray()); $fileObject = $file->openFile('r'); $fileObject->rewind(); $content = $fileObject->fread($file->getSize()); $encrypted = Crypt::encrypt($content); + Log::debug(sprintf('Full file length is %d and upload size is %d.', strlen($content), $file->getSize())); + Log::debug(sprintf('Encrypted content is %d', strlen($encrypted))); // store it: $this->uploadDisk->put($attachment->fileName(), $encrypted); @@ -202,6 +206,7 @@ class AttachmentHelper implements AttachmentHelperInterface /** * @codeCoverageIgnore + * * @param UploadedFile $file * * @return bool diff --git a/app/Helpers/Collection/BalanceLine.php b/app/Helpers/Collection/BalanceLine.php index 73b1a0f486..c3d892b214 100644 --- a/app/Helpers/Collection/BalanceLine.php +++ b/app/Helpers/Collection/BalanceLine.php @@ -147,13 +147,13 @@ class BalanceLine if ($this->getBudget() instanceof BudgetModel && !is_null($this->getBudget()->id)) { return $this->getBudget()->name; } - if ($this->getRole() == self::ROLE_DEFAULTROLE) { + if ($this->getRole() === self::ROLE_DEFAULTROLE) { return strval(trans('firefly.no_budget')); } - if ($this->getRole() == self::ROLE_TAGROLE) { + if ($this->getRole() === self::ROLE_TAGROLE) { return strval(trans('firefly.coveredWithTags')); } - if ($this->getRole() == self::ROLE_DIFFROLE) { + if ($this->getRole() === self::ROLE_DIFFROLE) { return strval(trans('firefly.leftUnbalanced')); } diff --git a/app/Helpers/Collection/Bill.php b/app/Helpers/Collection/Bill.php index e78e31631b..0df4a5fd5b 100644 --- a/app/Helpers/Collection/Bill.php +++ b/app/Helpers/Collection/Bill.php @@ -96,7 +96,7 @@ class Bill { $set = $this->bills->sortBy( function (BillLine $bill) { - $active = intval($bill->getBill()->active) == 0 ? 1 : 0; + $active = intval($bill->getBill()->active) === 0 ? 1 : 0; $name = $bill->getBill()->name; return $active . $name; diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/JournalCollector.php index 56d53d7446..43522ef149 100644 --- a/app/Helpers/Collector/JournalCollector.php +++ b/app/Helpers/Collector/JournalCollector.php @@ -60,17 +60,30 @@ class JournalCollector implements JournalCollectorInterface 'transaction_journals.description', 'transaction_journals.date', 'transaction_journals.encrypted', - 'transaction_currencies.code as transaction_currency_code', 'transaction_types.type as transaction_type_type', 'transaction_journals.bill_id', 'bills.name as bill_name', 'bills.name_encrypted as bill_name_encrypted', 'transactions.id as id', - 'transactions.amount as transaction_amount', + 'transactions.description as transaction_description', 'transactions.account_id', 'transactions.identifier', 'transactions.transaction_journal_id', + + 'transactions.amount as transaction_amount', + + 'transactions.transaction_currency_id as transaction_currency_id', + 'transaction_currencies.code as transaction_currency_code', + 'transaction_currencies.symbol as transaction_currency_symbol', + 'transaction_currencies.decimal_places as transaction_currency_dp', + + 'transactions.foreign_amount as transaction_foreign_amount', + 'transactions.foreign_currency_id as foreign_currency_id', + 'foreign_currencies.code as foreign_currency_code', + 'foreign_currencies.symbol as foreign_currency_symbol', + 'foreign_currencies.decimal_places as foreign_currency_dp', + 'accounts.name as account_name', 'accounts.encrypted as account_encrypted', 'account_types.type as account_type', @@ -484,17 +497,19 @@ class JournalCollector implements JournalCollectorInterface Log::debug('journalCollector::startQuery'); /** @var EloquentBuilder $query */ $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->leftJoin('transaction_currencies', 'transaction_currencies.id', 'transaction_journals.transaction_currency_id') ->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id') ->leftJoin('bills', 'bills.id', 'transaction_journals.bill_id') ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') ->leftJoin('account_types', 'accounts.account_type_id', 'account_types.id') + ->leftJoin('transaction_currencies', 'transaction_currencies.id', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', 'transactions.foreign_currency_id') ->whereNull('transactions.deleted_at') ->whereNull('transaction_journals.deleted_at') ->where('transaction_journals.user_id', $this->user->id) ->orderBy('transaction_journals.date', 'DESC') ->orderBy('transaction_journals.order', 'ASC') - ->orderBy('transaction_journals.id', 'DESC'); + ->orderBy('transaction_journals.id', 'DESC') + ->orderBy('transaction_journals.description', 'DESC'); $this->query = $query; diff --git a/app/Helpers/Filter/AmountFilter.php b/app/Helpers/Filter/AmountFilter.php index eb4ab55662..7fdd9eb966 100644 --- a/app/Helpers/Filter/AmountFilter.php +++ b/app/Helpers/Filter/AmountFilter.php @@ -53,4 +53,4 @@ class AmountFilter implements FilterInterface } ); } -} \ No newline at end of file +} diff --git a/app/Helpers/Filter/EmptyFilter.php b/app/Helpers/Filter/EmptyFilter.php index 23a4172e88..5fa9f64521 100644 --- a/app/Helpers/Filter/EmptyFilter.php +++ b/app/Helpers/Filter/EmptyFilter.php @@ -31,4 +31,4 @@ class EmptyFilter implements FilterInterface { return $set; } -} \ No newline at end of file +} diff --git a/app/Helpers/Filter/FilterInterface.php b/app/Helpers/Filter/FilterInterface.php index d2dcb9eda4..f189eb10ff 100644 --- a/app/Helpers/Filter/FilterInterface.php +++ b/app/Helpers/Filter/FilterInterface.php @@ -23,4 +23,4 @@ interface FilterInterface */ public function filter(Collection $set): Collection; -} \ No newline at end of file +} diff --git a/app/Helpers/Filter/InternalTransferFilter.php b/app/Helpers/Filter/InternalTransferFilter.php index 7e8d71588e..7ab06618aa 100644 --- a/app/Helpers/Filter/InternalTransferFilter.php +++ b/app/Helpers/Filter/InternalTransferFilter.php @@ -26,7 +26,7 @@ use Log; */ class InternalTransferFilter implements FilterInterface { - /** @var array */ + /** @var array */ private $accounts = []; /** @@ -70,4 +70,4 @@ class InternalTransferFilter implements FilterInterface } -} \ No newline at end of file +} diff --git a/app/Helpers/Filter/NegativeAmountFilter.php b/app/Helpers/Filter/NegativeAmountFilter.php index f2ef34bcc4..e1e494127e 100644 --- a/app/Helpers/Filter/NegativeAmountFilter.php +++ b/app/Helpers/Filter/NegativeAmountFilter.php @@ -44,4 +44,4 @@ class NegativeAmountFilter implements FilterInterface } ); } -} \ No newline at end of file +} diff --git a/app/Helpers/Filter/OpposingAccountFilter.php b/app/Helpers/Filter/OpposingAccountFilter.php index 69a1567943..663f9a35ce 100644 --- a/app/Helpers/Filter/OpposingAccountFilter.php +++ b/app/Helpers/Filter/OpposingAccountFilter.php @@ -60,4 +60,4 @@ class OpposingAccountFilter implements FilterInterface } ); } -} \ No newline at end of file +} diff --git a/app/Helpers/Filter/PositiveAmountFilter.php b/app/Helpers/Filter/PositiveAmountFilter.php index ddb71841da..e3d4065942 100644 --- a/app/Helpers/Filter/PositiveAmountFilter.php +++ b/app/Helpers/Filter/PositiveAmountFilter.php @@ -47,4 +47,4 @@ class PositiveAmountFilter implements FilterInterface } ); } -} \ No newline at end of file +} diff --git a/app/Helpers/Filter/TransferFilter.php b/app/Helpers/Filter/TransferFilter.php index d33d5e41ed..1aa0b5bd43 100644 --- a/app/Helpers/Filter/TransferFilter.php +++ b/app/Helpers/Filter/TransferFilter.php @@ -55,4 +55,4 @@ class TransferFilter implements FilterInterface return $new; } -} \ No newline at end of file +} diff --git a/app/Helpers/Report/BalanceReportHelper.php b/app/Helpers/Report/BalanceReportHelper.php index 9c62ea1ebc..dadef8fe41 100644 --- a/app/Helpers/Report/BalanceReportHelper.php +++ b/app/Helpers/Report/BalanceReportHelper.php @@ -244,7 +244,7 @@ class BalanceReportHelper implements BalanceReportHelperInterface foreach ($accounts as $account) { $leftEntry = $tagsLeft->filter( function (Tag $tag) use ($account) { - return $tag->account_id == $account->id; + return $tag->account_id === $account->id; } ); $left = '0'; diff --git a/app/Helpers/Report/BudgetReportHelper.php b/app/Helpers/Report/BudgetReportHelper.php index 55c6ea64a7..7fecb90c89 100644 --- a/app/Helpers/Report/BudgetReportHelper.php +++ b/app/Helpers/Report/BudgetReportHelper.php @@ -56,7 +56,7 @@ class BudgetReportHelper implements BudgetReportHelperInterface /** @var Budget $budget */ foreach ($set as $budget) { $budgetLimits = $this->repository->getBudgetLimits($budget, $start, $end); - if ($budgetLimits->count() == 0) { // no budget limit(s) for this budget + if ($budgetLimits->count() === 0) { // no budget limit(s) for this budget $spent = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end);// spent for budget in time range if (bccomp($spent, '0') === -1) { diff --git a/app/Helpers/Report/PopupReport.php b/app/Helpers/Report/PopupReport.php index f4d0a07d17..7823eb95b8 100644 --- a/app/Helpers/Report/PopupReport.php +++ b/app/Helpers/Report/PopupReport.php @@ -187,7 +187,7 @@ class PopupReport implements PopupReportInterface $journals = $journals->filter( function (Transaction $transaction) use ($report) { // get the destinations: - $destinations = $transaction->destinationAccountList($transaction->transactionJournal)->pluck('id')->toArray(); + $destinations = $transaction->transactionJournal->destinationAccountList()->pluck('id')->toArray(); // do these intersect with the current list? return !empty(array_intersect($report, $destinations)); @@ -196,4 +196,4 @@ class PopupReport implements PopupReportInterface return $journals; } -} \ No newline at end of file +} diff --git a/app/Helpers/Report/PopupReportInterface.php b/app/Helpers/Report/PopupReportInterface.php index 79ca4be007..388c1a1d91 100644 --- a/app/Helpers/Report/PopupReportInterface.php +++ b/app/Helpers/Report/PopupReportInterface.php @@ -80,4 +80,4 @@ interface PopupReportInterface * @return Collection */ public function byIncome(Account $account, array $attributes): Collection; -} \ No newline at end of file +} diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 8e248a1628..24e04b7f7b 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -219,8 +219,8 @@ class AccountController extends Controller $start->subDay(); $ids = $accounts->pluck('id')->toArray(); - $startBalances = Steam::balancesById($ids, $start); - $endBalances = Steam::balancesById($ids, $end); + $startBalances = Steam::balancesByAccounts($accounts, $start); + $endBalances = Steam::balancesByAccounts($accounts, $end); $activities = Steam::getLastActivities($ids); $accounts->each( @@ -293,8 +293,8 @@ class AccountController extends Controller $periods = $this->getPeriodOverview($account); } - $count = 0; - $loop = 0; + $count = 0; + $loop = 0; // grab journals, but be prepared to jump a period back to get the right ones: Log::info('Now at loop start.'); while ($count === 0 && $loop < 3) { @@ -308,7 +308,7 @@ class AccountController extends Controller $journals = $collector->getPaginatedJournals(); $journals->setPath('accounts/show/' . $account->id . '/' . $moment); $count = $journals->getCollection()->count(); - if ($count === 0) { + if ($count === 0 && $loop < 3) { $start->subDay(); $start = Navigation::startOfPeriod($start, $range); $end = Navigation::endOfPeriod($start, $range); @@ -316,7 +316,7 @@ class AccountController extends Controller } } - if ($moment != 'all' && $loop > 1) { + if ($moment !== 'all' && $loop > 1) { $subTitle = trans( 'firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php index cd8de38752..6827d58fe2 100644 --- a/app/Http/Controllers/AttachmentController.php +++ b/app/Http/Controllers/AttachmentController.php @@ -98,10 +98,13 @@ class AttachmentController extends Controller */ public function download(AttachmentRepositoryInterface $repository, Attachment $attachment) { + + if ($repository->exists($attachment)) { $content = $repository->getContent($attachment); $quoted = sprintf('"%s"', addcslashes(basename($attachment->filename), '"\\')); + /** @var LaravelResponse $response */ $response = response($content, 200); $response @@ -149,7 +152,8 @@ class AttachmentController extends Controller { $image = 'images/page_green.png'; - if ($attachment->mime == 'application/pdf') { + + if ($attachment->mime === 'application/pdf') { $image = 'images/page_white_acrobat.png'; } $file = public_path($image); diff --git a/app/Http/Controllers/BillController.php b/app/Http/Controllers/BillController.php index 2418bde1b0..8a963cd57f 100644 --- a/app/Http/Controllers/BillController.php +++ b/app/Http/Controllers/BillController.php @@ -175,7 +175,7 @@ class BillController extends Controller */ public function rescan(Request $request, BillRepositoryInterface $repository, Bill $bill) { - if (intval($bill->active) == 0) { + if (intval($bill->active) === 0) { $request->session()->flash('warning', strval(trans('firefly.cannot_scan_inactive_bill'))); return redirect(URL::previous()); @@ -206,7 +206,7 @@ class BillController extends Controller /** @var Carbon $date */ $date = session('start'); $year = $date->year; - $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $yearAverage = $repository->getYearAverage($bill, $date); $overallAverage = $repository->getOverallAverage($bill); diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index 05188a146e..b223c477dc 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -15,6 +15,7 @@ namespace FireflyIII\Http\Controllers; use Amount; use Carbon\Carbon; +use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Requests\BudgetFormRequest; @@ -80,7 +81,7 @@ class BudgetController extends Controller /** @var Carbon $end */ $end = session('end', Carbon::now()->endOfMonth()); $budgetLimit = $this->repository->updateLimitAmount($budget, $start, $end, $amount); - if ($amount == 0) { + if ($amount === 0) { $budgetLimit = null; } Preferences::mark(); @@ -166,16 +167,38 @@ class BudgetController extends Controller } /** + * @param string|null $moment + * * @return View */ - public function index() + public function index(string $moment = null) { + $range = Preferences::get('viewRange', '1M')->data; + $start = session('start', new Carbon); + $end = session('end', new Carbon); + + // make date if present: + if (!is_null($moment) || strlen(strval($moment)) !== 0) { + try { + $start = new Carbon($moment); + $end = Navigation::endOfPeriod($start, $range); + } catch (Exception $e) { + // start and end are already defined. + + } + } + $next = clone $end; + $next->addDay(); + $prev = clone $start; + $prev->subDay(); + $prev = Navigation::startOfPeriod($prev, $range); + + $this->repository->cleanupBudgets(); + $budgets = $this->repository->getActiveBudgets(); $inactive = $this->repository->getInactiveBudgets(); - $start = session('start', new Carbon); - $end = session('end', new Carbon); $periodStart = $start->formatLocalized($this->monthAndDayFormat); $periodEnd = $end->formatLocalized($this->monthAndDayFormat); $budgetInformation = $this->collectBudgetInformation($budgets, $start, $end); @@ -184,9 +207,44 @@ class BudgetController extends Controller $spent = array_sum(array_column($budgetInformation, 'spent')); $budgeted = array_sum(array_column($budgetInformation, 'budgeted')); + // select thing for last 12 periods: + $previousLoop = []; + $previousDate = clone $start; + $count = 0; + while ($count < 12) { + $previousDate->subDay(); + $previousDate = Navigation::startOfPeriod($previousDate, $range); + $format = $previousDate->format('Y-m-d'); + $previousLoop[$format] = Navigation::periodShow($previousDate, $range); + $count++; + } + + // select thing for next 12 periods: + $nextLoop = []; + $nextDate = clone $end; + $nextDate->addDay(); + $count = 0; + + while ($count < 12) { + $format = $nextDate->format('Y-m-d'); + $nextLoop[$format] = Navigation::periodShow($nextDate, $range); + $nextDate = Navigation::endOfPeriod($nextDate, $range); + $count++; + $nextDate->addDay(); + } + + // display info + $currentMonth = Navigation::periodShow($start, $range); + $nextText = Navigation::periodShow($next, $range); + $prevText = Navigation::periodShow($prev, $range); + return view( 'budgets.index', - compact('available', 'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets', 'spent', 'budgeted') + compact( + 'available', 'currentMonth', 'next', 'nextText', 'prev', 'prevText', + 'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets', + 'spent', 'budgeted', 'previousLoop', 'nextLoop', 'start' + ) ); } @@ -235,7 +293,7 @@ class BudgetController extends Controller ); } - $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $count = 0; @@ -244,7 +302,7 @@ class BudgetController extends Controller Log::info('Now at no-budget loop start.'); while ($count === 0 && $loop < 3) { $loop++; - Log::info('Count is zero, search for journals.'); + Log::info(sprintf('Count is zero, search for journals between %s and %s.', $start->format('Y-m-d'), $end->format('Y-m-d'))); /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class); $collector->setAllAssetAccounts()->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setLimit($pageSize)->setPage($page) @@ -252,7 +310,7 @@ class BudgetController extends Controller $journals = $collector->getPaginatedJournals(); $journals->setPath('/budgets/list/no-budget'); $count = $journals->getCollection()->count(); - if ($count === 0) { + if ($count === 0 && $loop < 3) { $start->subDay(); $start = Navigation::startOfPeriod($start, $range); $end = Navigation::endOfPeriod($start, $range); @@ -260,7 +318,7 @@ class BudgetController extends Controller } } - if ($moment != 'all' && $loop > 1) { + if ($moment !== 'all' && $loop > 1) { $subTitle = trans( 'firefly.without_budget_between', ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] @@ -299,7 +357,7 @@ class BudgetController extends Controller /** @var Carbon $start */ $start = session('first', Carbon::create()->startOfYear()); $end = new Carbon; - $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $limits = $this->getLimits($budget, $start, $end); $repetition = null; @@ -326,11 +384,11 @@ class BudgetController extends Controller */ public function showByBudgetLimit(Request $request, Budget $budget, BudgetLimit $budgetLimit) { - if ($budgetLimit->budget->id != $budget->id) { + if ($budgetLimit->budget->id !== $budget->id) { throw new FireflyException('This budget limit is not part of this budget.'); } - $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $subTitle = trans( 'firefly.budget_in_period', [ diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index a760a0733d..4cf4389a2a 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -199,7 +199,7 @@ class CategoryController extends Controller ); } - $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $count = 0; @@ -216,7 +216,7 @@ class CategoryController extends Controller $journals = $collector->getPaginatedJournals(); $journals->setPath('/categories/list/no-category'); $count = $journals->getCollection()->count(); - if ($count === 0) { + if ($count === 0 && $loop < 3) { $start->subDay(); $start = Navigation::startOfPeriod($start, $range); $end = Navigation::endOfPeriod($start, $range); @@ -224,7 +224,7 @@ class CategoryController extends Controller } } - if ($moment != 'all' && $loop > 1) { + if ($moment !== 'all' && $loop > 1) { $subTitle = trans( 'firefly.without_category_between', ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] @@ -247,7 +247,7 @@ class CategoryController extends Controller // default values: $subTitle = $category->name; $subTitleIcon = 'fa-bar-chart'; - $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $count = 0; $loop = 0; @@ -300,7 +300,7 @@ class CategoryController extends Controller $journals = $collector->getPaginatedJournals(); $journals->setPath('categories/show/' . $category->id); $count = $journals->getCollection()->count(); - if ($count === 0) { + if ($count === 0 && $loop < 3) { $start->subDay(); $start = Navigation::startOfPeriod($start, $range); $end = Navigation::endOfPeriod($start, $range); @@ -308,7 +308,7 @@ class CategoryController extends Controller } } - if ($moment != 'all' && $loop > 1) { + if ($moment !== 'all' && $loop > 1) { $subTitle = trans( 'firefly.journals_in_period_for_category', ['name' => $category->name, 'start' => $start->formatLocalized($this->monthAndDayFormat), @@ -463,7 +463,7 @@ class CategoryController extends Controller $accountRepository = app(AccountRepositoryInterface::class); $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); $first = $repository->firstUseDate($category); - if ($first->year == 1900) { + if ($first->year === 1900) { $first = new Carbon; } $range = Preferences::get('viewRange', '1M')->data; diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index 38bd1f3345..61194faff9 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -14,7 +14,6 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Chart; use Carbon\Carbon; -use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Helpers\Collector\JournalCollectorInterface; @@ -115,9 +114,8 @@ class AccountController extends Controller $start->subDay(); $accounts = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]); - $ids = $accounts->pluck('id')->toArray(); - $startBalances = Steam::balancesById($ids, $start); - $endBalances = Steam::balancesById($ids, $end); + $startBalances = Steam::balancesByAccounts($accounts, $start); + $endBalances = Steam::balancesByAccounts($accounts, $end); $chartData = []; foreach ($accounts as $account) { @@ -129,6 +127,7 @@ class AccountController extends Controller $chartData[$account->name] = $diff; } } + arsort($chartData); $data = $this->generator->singleSet(strval(trans('firefly.spent')), $chartData); $cache->store($data); @@ -336,7 +335,7 @@ class AccountController extends Controller /** * @param Account $account - * @param Carbon $start + * @param Carbon $start * * @return \Illuminate\Http\JsonResponse * @throws FireflyException @@ -411,9 +410,8 @@ class AccountController extends Controller $accounts = $repository->getAccountsByType([AccountType::REVENUE]); $start->subDay(); - $ids = $accounts->pluck('id')->toArray(); - $startBalances = Steam::balancesById($ids, $start); - $endBalances = Steam::balancesById($ids, $end); + $startBalances = Steam::balancesByAccounts($accounts, $start); + $endBalances = Steam::balancesByAccounts($accounts, $end); foreach ($accounts as $account) { $id = $account->id; @@ -427,7 +425,7 @@ class AccountController extends Controller } arsort($chartData); - $data = $this->generator->singleSet(strval(trans('firefly.spent')), $chartData); + $data = $this->generator->singleSet(strval(trans('firefly.earned')), $chartData); $cache->store($data); return Response::json($data); diff --git a/app/Http/Controllers/Chart/BudgetController.php b/app/Http/Controllers/Chart/BudgetController.php index 189c1e2dea..4b74cfbf38 100644 --- a/app/Http/Controllers/Chart/BudgetController.php +++ b/app/Http/Controllers/Chart/BudgetController.php @@ -31,6 +31,7 @@ use Illuminate\Support\Collection; use Navigation; use Preferences; use Response; +use Steam; /** * Class BudgetController @@ -123,7 +124,7 @@ class BudgetController extends Controller */ public function budgetLimit(Budget $budget, BudgetLimit $budgetLimit) { - if ($budgetLimit->budget->id != $budget->id) { + if ($budgetLimit->budget->id !== $budget->id) { throw new FireflyException('This budget limit is not part of this budget.'); } @@ -320,12 +321,12 @@ class BudgetController extends Controller ['label' => strval(trans('firefly.overspent')), 'entries' => [], 'type' => 'bar',], ]; - /** @var Budget $budget */ foreach ($budgets as $budget) { // get relevant repetitions: $limits = $this->repository->getBudgetLimits($budget, $start, $end); $expenses = $this->getExpensesForBudget($limits, $budget, $start, $end); + foreach ($expenses as $name => $row) { $chartData[0]['entries'][$name] = $row['spent']; $chartData[1]['entries'][$name] = $row['left']; @@ -529,9 +530,7 @@ class BudgetController extends Controller $rows = $this->spentInPeriodMulti($budget, $limits); foreach ($rows as $name => $row) { if (bccomp($row['spent'], '0') !== 0 || bccomp($row['left'], '0') !== 0) { - $return[$name]['spent'] = bcmul($row['spent'], '-1'); - $return[$name]['left'] = $row['left']; - $return[$name]['overspent'] = bcmul($row['overspent'], '-1'); + $return[$name] = $row; } } unset($rows, $row); @@ -563,6 +562,7 @@ class BudgetController extends Controller /** @var BudgetLimit $budgetLimit */ foreach ($limits as $budgetLimit) { $expenses = $this->repository->spentInPeriod(new Collection([$budget]), new Collection, $budgetLimit->start_date, $budgetLimit->end_date); + $expenses = Steam::positive($expenses); if ($limits->count() > 1) { $name = $budget->name . ' ' . trans( @@ -578,10 +578,14 @@ class BudgetController extends Controller * left: amount of budget limit min spent, or 0 when < 0. * spent: spent, or amount of budget limit when > amount */ - $amount = $budgetLimit->amount; - $left = bccomp(bcadd($amount, $expenses), '0') < 1 ? '0' : bcadd($amount, $expenses); - $spent = bccomp($expenses, $amount) === 1 ? $expenses : bcmul($amount, '-1'); - $overspent = bccomp(bcadd($amount, $expenses), '0') < 1 ? bcadd($amount, $expenses) : '0'; + $amount = $budgetLimit->amount; + $leftInLimit = bcsub($amount, $expenses); + $hasOverspent = bccomp($leftInLimit, '0') === -1; + + $left = $hasOverspent ? '0' : bcsub($amount, $expenses); + $spent = $hasOverspent ? $amount : $expenses; + $overspent = $hasOverspent ? Steam::positive($leftInLimit) : '0'; + $return[$name] = [ 'left' => $left, 'overspent' => $overspent, diff --git a/app/Http/Controllers/Chart/CategoryController.php b/app/Http/Controllers/Chart/CategoryController.php index d330a9b363..b6ac5e0973 100644 --- a/app/Http/Controllers/Chart/CategoryController.php +++ b/app/Http/Controllers/Chart/CategoryController.php @@ -67,7 +67,7 @@ class CategoryController extends Controller $start = $repository->firstUseDate($category); - if ($start->year == 1900) { + if ($start->year === 1900) { $start = new Carbon; } @@ -277,10 +277,10 @@ class CategoryController extends Controller */ public function specificPeriod(CategoryRepositoryInterface $repository, Category $category, Carbon $date) { - $range = Preferences::get('viewRange', '1M')->data; - $start = Navigation::startOfPeriod($date, $range); - $end = Navigation::endOfPeriod($date, $range); - $data = $this->makePeriodChart($repository, $category, $start, $end); + $range = Preferences::get('viewRange', '1M')->data; + $start = Navigation::startOfPeriod($date, $range); + $end = Navigation::endOfPeriod($date, $range); + $data = $this->makePeriodChart($repository, $category, $start, $end); return Response::json($data); } @@ -336,9 +336,9 @@ class CategoryController extends Controller $sum = bcadd($spent, $earned); $label = trim(Navigation::periodShow($start, '1D')); - $chartData[0]['entries'][$label] = round(bcmul($spent, '-1'),12); - $chartData[1]['entries'][$label] = round($earned,12); - $chartData[2]['entries'][$label] = round($sum,12); + $chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 12); + $chartData[1]['entries'][$label] = round($earned, 12); + $chartData[2]['entries'][$label] = round($sum, 12); $start->addDay(); diff --git a/app/Http/Controllers/Chart/CategoryReportController.php b/app/Http/Controllers/Chart/CategoryReportController.php index 572a8a3432..a79073c863 100644 --- a/app/Http/Controllers/Chart/CategoryReportController.php +++ b/app/Http/Controllers/Chart/CategoryReportController.php @@ -16,7 +16,6 @@ namespace FireflyIII\Http\Controllers\Chart; use Carbon\Carbon; use FireflyIII\Generator\Chart\Basic\GeneratorInterface; -use FireflyIII\Generator\Report\Category\MonthReportGenerator; use FireflyIII\Helpers\Chart\MetaPieChartInterface; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Helpers\Filter\NegativeAmountFilter; @@ -248,7 +247,7 @@ class CategoryReportController extends Controller // remove all empty entries to prevent cluttering: $newSet = []; foreach ($chartData as $key => $entry) { - if (!array_sum($entry['entries']) == 0) { + if (!array_sum($entry['entries']) === 0) { $newSet[$key] = $chartData[$key]; } } diff --git a/app/Http/Controllers/Chart/ReportController.php b/app/Http/Controllers/Chart/ReportController.php index 1452192fe3..7df9b25e58 100644 --- a/app/Http/Controllers/Chart/ReportController.php +++ b/app/Http/Controllers/Chart/ReportController.php @@ -67,11 +67,10 @@ class ReportController extends Controller if ($cache->has()) { return Response::json($cache->get()); // @codeCoverageIgnore } - $ids = $accounts->pluck('id')->toArray(); $current = clone $start; $chartData = []; while ($current < $end) { - $balances = Steam::balancesById($ids, $current); + $balances = Steam::balancesByAccounts($accounts, $current); $sum = $this->arraySum($balances); $label = $current->formatLocalized(strval(trans('config.month_and_day'))); $chartData[$label] = $sum; @@ -104,7 +103,7 @@ class ReportController extends Controller $cache->addProperty($accounts); $cache->addProperty($end); if ($cache->has()) { - //return Response::json($cache->get()); // @codeCoverageIgnore + return Response::json($cache->get()); // @codeCoverageIgnore } Log::debug('Going to do operations for accounts ', $accounts->pluck('id')->toArray()); $format = Navigation::preferredCarbonLocalizedFormat($start, $end); @@ -250,7 +249,7 @@ class ReportController extends Controller $cache->addProperty($accounts); $cache->addProperty($end); if ($cache->has()) { - // return $cache->get(); // @codeCoverageIgnore + return $cache->get(); // @codeCoverageIgnore } $currentStart = clone $start; diff --git a/app/Http/Controllers/Chart/TagReportController.php b/app/Http/Controllers/Chart/TagReportController.php index 2aca50bc77..d7e86825b6 100644 --- a/app/Http/Controllers/Chart/TagReportController.php +++ b/app/Http/Controllers/Chart/TagReportController.php @@ -231,7 +231,7 @@ class TagReportController extends Controller // remove all empty entries to prevent cluttering: $newSet = []; foreach ($chartData as $key => $entry) { - if (!array_sum($entry['entries']) == 0) { + if (!array_sum($entry['entries']) === 0) { $newSet[$key] = $chartData[$key]; } } @@ -364,4 +364,4 @@ class TagReportController extends Controller return $grouped; } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 63cbe9c975..d46c485086 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -18,6 +18,7 @@ use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; +use FireflyIII\Support\Facades\Preferences; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; @@ -25,6 +26,7 @@ use Illuminate\Routing\Controller as BaseController; use Session; use URL; use View; +use Route; /** * Class Controller @@ -63,6 +65,12 @@ class Controller extends BaseController $this->monthAndDayFormat = (string)trans('config.month_and_day'); $this->dateTimeFormat = (string)trans('config.date_time'); + // get shown-intro-preference: + $key = 'shown_demo_' . Route::currentRouteName(); + $shownDemo = Preferences::get($key, false)->data; + View::share('shownDemo', $shownDemo); + View::share('current_route_name', Route::currentRouteName()); + return $next($request); } ); diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 7e4cca9547..6ea0eb2119 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -21,9 +21,11 @@ use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use Illuminate\Http\Request; +use Illuminate\Routing\Route; use Illuminate\Support\Collection; use Log; use Preferences; +use Route as RouteFacade; use Session; use View; @@ -107,7 +109,7 @@ class HomeController extends Controller $types = config('firefly.accountTypesByIdentifier.asset'); $count = $repository->count($types); - if ($count == 0) { + if ($count === 0) { return redirect(route('new-user.index')); } @@ -120,7 +122,6 @@ class HomeController extends Controller $start = session('start', Carbon::now()->startOfMonth()); /** @var Carbon $end */ $end = session('end', Carbon::now()->endOfMonth()); - $showTour = Preferences::get('tour', true)->data; $accounts = $repository->getAccountsById($frontPage->data); $showDepositsFrontpage = Preferences::get('showDepositsFrontpage', false)->data; @@ -137,10 +138,34 @@ class HomeController extends Controller } return view( - 'index', compact('count', 'showTour', 'title', 'subTitle', 'mainTitleIcon', 'transactions', 'showDepositsFrontpage', 'billCount') + 'index', compact('count', 'title', 'subTitle', 'mainTitleIcon', 'transactions', 'showDepositsFrontpage', 'billCount') ); } + public function routes() + { + $set = RouteFacade::getRoutes(); + $ignore = ['chart.','javascript.','json.','report-data.','popup.','debugbar.']; + /** @var Route $route */ + foreach ($set as $route) { + $name = $route->getName(); + if (!is_null($name) && in_array('GET', $route->methods()) && strlen($name) > 0) { + $found = false; + foreach ($ignore as $string) { + if (strpos($name, $string) !== false) { + $found = true; + } + } + if (!$found) { + echo 'touch '.$route->getName() . '.md;'; + } + + } + } + + return ' '; + } + /** * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index 325698f720..c4b0bd1342 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -12,32 +12,29 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers; -use Crypt; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Requests\ImportUploadRequest; -use FireflyIII\Import\ImportProcedureInterface; -use FireflyIII\Import\Setup\SetupInterface; +use FireflyIII\Import\Configurator\ConfiguratorInterface; +use FireflyIII\Import\Routine\ImportRoutine; use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; -use FireflyIII\Repositories\User\UserRepositoryInterface; use Illuminate\Http\Request; use Illuminate\Http\Response as LaravelResponse; use Log; use Response; -use Session; -use SplFileObject; -use Storage; -use Symfony\Component\HttpFoundation\File\UploadedFile; use View; /** - * Class ImportController + * Class ImportController. * * @package FireflyIII\Http\Controllers */ class ImportController extends Controller { + /** @var ImportJobRepositoryInterface */ + public $repository; + /** * */ @@ -48,7 +45,8 @@ class ImportController extends Controller $this->middleware( function ($request, $next) { View::share('mainTitleIcon', 'fa-archive'); - View::share('title', trans('firefly.import_data_full')); + View::share('title', trans('firefly.import_index_title')); + $this->repository = app(ImportJobRepositoryInterface::class); return $next($request); } @@ -56,28 +54,7 @@ class ImportController extends Controller } /** - * This is the last step before the import starts. - * - * @param ImportJob $job - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View - */ - public function complete(ImportJob $job) - { - Log::debug('Now in complete()', ['job' => $job->key]); - if (!$this->jobInCorrectStep($job, 'complete')) { - return $this->redirectToCorrectStep($job); - } - $subTitle = trans('firefly.import_complete'); - $subTitleIcon = 'fa-star'; - - return view('import.complete', compact('job', 'subTitle', 'subTitleIcon')); - } - - /** - * This is step 3. - * This is the first step in configuring the job. It can only be executed - * when the job is set to "import_status_never_started". + * This is step 3. This repeats until the job is configured. * * @param ImportJob $job * @@ -86,37 +63,43 @@ class ImportController extends Controller */ public function configure(ImportJob $job) { - Log::debug('Now at start of configure()'); - if (!$this->jobInCorrectStep($job, 'configure')) { - return $this->redirectToCorrectStep($job); - } + // create configuration class: + $configurator = $this->makeConfigurator($job); - // actual code - $importer = $this->makeImporter($job); - $importer->configure(); - $data = $importer->getConfigurationData(); - $subTitle = trans('firefly.configure_import'); + // is the job already configured? + if ($configurator->isJobConfigured()) { + $this->repository->updateStatus($job, 'configured'); + + return redirect(route('import.status', [$job->key])); + } + $view = $configurator->getNextView(); + $data = $configurator->getNextData(); + $subTitle = trans('firefly.import_config_bread_crumb'); $subTitleIcon = 'fa-wrench'; - return view('import.' . $job->file_type . '.configure', compact('data', 'job', 'subTitle', 'subTitleIcon')); + return view($view, compact('data', 'job', 'subTitle', 'subTitleIcon')); } /** - * Generate a JSON file of the job's config and send it to the user. + * Generate a JSON file of the job's configuration and send it to the user. * * @param ImportJob $job * - * @return mixed + * @return LaravelResponse */ public function download(ImportJob $job) { Log::debug('Now in download()', ['job' => $job->key]); - $config = $job->configuration; + $config = $job->configuration; + + // TODO this is CSV import specific: $config['column-roles-complete'] = false; $config['column-mapping-complete'] = false; + $config['initial-config-complete'] = false; $config['delimiter'] = $config['delimiter'] === "\t" ? 'tab' : $config['delimiter']; - $result = json_encode($config, JSON_PRETTY_PRINT); - $name = sprintf('"%s"', addcslashes('import-configuration-' . date('Y-m-d') . '.json', '"\\')); + + $result = json_encode($config, JSON_PRETTY_PRINT); + $name = sprintf('"%s"', addcslashes('import-configuration-' . date('Y-m-d') . '.json', '"\\')); /** @var LaravelResponse $response */ $response = response($result, 200); @@ -134,26 +117,6 @@ class ImportController extends Controller } - /** - * @param ImportJob $job - * - * @return View - */ - public function finished(ImportJob $job) - { - if (!$this->jobInCorrectStep($job, 'finished')) { - return $this->redirectToCorrectStep($job); - } - - // if there is a tag (there might not be), we can link to it: - $tagId = $job->extended_status['importTag'] ?? 0; - - $subTitle = trans('firefly.import_finished'); - $subTitleIcon = 'fa-star'; - - return view('import.finished', compact('job', 'subTitle', 'subTitleIcon', 'tagId')); - } - /** * This is step 1. Upload a file. * @@ -161,8 +124,7 @@ class ImportController extends Controller */ public function index() { - Log::debug('Now at index'); - $subTitle = trans('firefly.import_data_index'); + $subTitle = trans('firefly.import_index_sub_title'); $subTitleIcon = 'fa-home'; $importFileTypes = []; $defaultImportType = config('firefly.default_import_format'); @@ -175,42 +137,75 @@ class ImportController extends Controller } /** + * This is step 2. It creates an Import Job. Stores the import. + * + * @param ImportUploadRequest $request + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function initialize(ImportUploadRequest $request) + { + Log::debug('Now in initialize()'); + + // create import job: + $type = $request->get('import_file_type'); + $job = $this->repository->create($type); + Log::debug('Created new job', ['key' => $job->key, 'id' => $job->id]); + + // process file: + $this->repository->processFile($job, $request->files->get('import_file')); + + // process config, if present: + if ($request->files->has('configuration_file')) { + $this->repository->processConfiguration($job, $request->files->get('configuration_file')); + } + + $this->repository->updateStatus($job, 'initialized'); + + return redirect(route('import.configure', [$job->key])); + } + + /** + * + * Show status of import job in JSON. + * * @param ImportJob $job * * @return \Illuminate\Http\JsonResponse */ public function json(ImportJob $job) { - $result = [ - 'showPercentage' => false, - 'started' => false, - 'finished' => false, - 'running' => false, - 'errors' => $job->extended_status['errors'], - 'percentage' => 0, - 'steps' => $job->extended_status['total_steps'], - 'stepsDone' => $job->extended_status['steps_done'], - 'statusText' => trans('firefly.import_status_' . $job->status), - 'finishedText' => '', + $result = [ + 'started' => false, + 'finished' => false, + 'running' => false, + 'errors' => array_values($job->extended_status['errors']), + 'percentage' => 0, + 'show_percentage' => false, + 'steps' => $job->extended_status['steps'], + 'done' => $job->extended_status['done'], + 'statusText' => trans('firefly.import_status_job_' . $job->status), + 'status' => $job->status, + 'finishedText' => '', ]; - $percentage = 0; - if ($job->extended_status['total_steps'] !== 0) { - $percentage = round(($job->extended_status['steps_done'] / $job->extended_status['total_steps']) * 100, 0); + + if ($job->extended_status['steps'] !== 0) { + $result['percentage'] = round(($job->extended_status['done'] / $job->extended_status['steps']) * 100, 0); + $result['show_percentage'] = true; } - if ($job->status === 'import_complete') { - $tagId = $job->extended_status['importTag']; + + if ($job->status === 'finished') { + $tagId = $job->extended_status['tag']; /** @var TagRepositoryInterface $repository */ $repository = app(TagRepositoryInterface::class); $tag = $repository->find($tagId); $result['finished'] = true; - $result['finishedText'] = trans('firefly.import_finished_link', ['link' => route('tags.show', [$tag->id]), 'tag' => $tag->tag]); + $result['finishedText'] = trans('firefly.import_status_finished_job', ['link' => route('tags.show', [$tag->id, 'all']), 'tag' => $tag->tag]); } - if ($job->status === 'import_running') { - $result['started'] = true; - $result['running'] = true; - $result['showPercentage'] = true; - $result['percentage'] = $percentage; + if ($job->status === 'running') { + $result['started'] = true; + $result['running'] = true; } return Response::json($result); @@ -219,286 +214,82 @@ class ImportController extends Controller /** * Step 4. Save the configuration. * - * @param Request $request - * @param ImportJobRepositoryInterface $repository - * @param ImportJob $job - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - */ - public function postConfigure(Request $request, ImportJobRepositoryInterface $repository, ImportJob $job) - { - Log::debug('Now in postConfigure()', ['job' => $job->key]); - if (!$this->jobInCorrectStep($job, 'process')) { - return $this->redirectToCorrectStep($job); - } - Log::debug('Continue postConfigure()', ['job' => $job->key]); - - // actual code - $importer = $this->makeImporter($job); - $data = $request->all(); - $files = $request->files; - $importer->saveImportConfiguration($data, $files); - - // update job: - $repository->updateStatus($job, 'import_configuration_saved'); - - // return redirect to settings. - // this could loop until the user is done. - return redirect(route('import.settings', [$job->key])); - } - - /** - * This step 6. Depending on the importer, this will process the - * settings given and store them. - * * @param Request $request * @param ImportJob $job * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - * @throws FireflyException */ - public function postSettings(Request $request, ImportJob $job) + public function postConfigure(Request $request, ImportJob $job) { - Log::debug('Now in postSettings()', ['job' => $job->key]); - if (!$this->jobInCorrectStep($job, 'store-settings')) { - return $this->redirectToCorrectStep($job); - } - $importer = $this->makeImporter($job); - $importer->storeSettings($request); - - // return redirect to settings (for more settings perhaps) - return redirect(route('import.settings', [$job->key])); - } - - /** - * Step 5. Depending on the importer, this will show the user settings to - * fill in. - * - * @param ImportJobRepositoryInterface $repository - * @param ImportJob $job - * - * @return View - */ - public function settings(ImportJobRepositoryInterface $repository, ImportJob $job) - { - Log::debug('Now in settings()', ['job' => $job->key]); - if (!$this->jobInCorrectStep($job, 'settings')) { - return $this->redirectToCorrectStep($job); - } - Log::debug('Continue in settings()'); - $importer = $this->makeImporter($job); - $subTitle = trans('firefly.settings_for_import'); - $subTitleIcon = 'fa-wrench'; - - // now show settings screen to user. - if ($importer->requireUserSettings()) { - Log::debug('Job requires user config.'); - $data = $importer->getDataForSettings(); - $view = $importer->getViewForSettings(); - - return view($view, compact('data', 'job', 'subTitle', 'subTitleIcon')); - } - Log::debug('Job does NOT require user config.'); - - $repository->updateStatus($job, 'settings_complete'); - - // if no more settings, save job and continue to process thing. - return redirect(route('import.complete', [$job->key])); - - // ask the importer for the requested action. - // for example pick columns or map data. - // depends of course on the data in the job. - } - - /** - * @param ImportProcedureInterface $importProcedure - * @param ImportJob $job - */ - public function start(ImportProcedureInterface $importProcedure, ImportJob $job) - { - set_time_limit(0); - if ($job->status == 'settings_complete') { - $importProcedure->runImport($job); - } - } - - /** - * This is the last step before the import starts. - * - * @param ImportJob $job - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View - */ - public function status(ImportJob $job) - { // - Log::debug('Now in status()', ['job' => $job->key]); - if (!$this->jobInCorrectStep($job, 'status')) { - return $this->redirectToCorrectStep($job); - } - $subTitle = trans('firefly.import_status'); - $subTitleIcon = 'fa-star'; - - return view('import.status', compact('job', 'subTitle', 'subTitleIcon')); - } - - - /** - * This is step 2. It creates an Import Job. Stores the import. - * - * @param ImportUploadRequest $request - * @param ImportJobRepositoryInterface $repository - * @param UserRepositoryInterface $userRepository - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - */ - public function upload(ImportUploadRequest $request, ImportJobRepositoryInterface $repository, UserRepositoryInterface $userRepository) - { - Log::debug('Now in upload()'); - // create import job: - $type = $request->get('import_file_type'); - $job = $repository->create($type); - Log::debug('Created new job', ['key' => $job->key, 'id' => $job->id]); - - /** @var UploadedFile $upload */ - $upload = $request->files->get('import_file'); - $newName = $job->key . '.upload'; - $uploaded = new SplFileObject($upload->getRealPath()); - $content = $uploaded->fread($uploaded->getSize()); - $contentEncrypted = Crypt::encrypt($content); - $disk = Storage::disk('upload'); - - // user is demo user, replace upload with prepared file. - if ($userRepository->hasRole(auth()->user(), 'demo')) { - $stubsDisk = Storage::disk('stubs'); - $content = $stubsDisk->get('demo-import.csv'); - $contentEncrypted = Crypt::encrypt($content); - $disk->put($newName, $contentEncrypted); - Log::debug('Replaced upload with demo file.'); - - // also set up prepared configuration. - $configuration = json_decode($stubsDisk->get('demo-configuration.json'), true); - $repository->setConfiguration($job, $configuration); - Log::debug('Set configuration for demo user', $configuration); - - // also flash info - Session::flash('info', trans('demo.import-configure-security')); - } - if (!$userRepository->hasRole(auth()->user(), 'demo')) { - // user is not demo, process original upload: - $disk->put($newName, $contentEncrypted); - Log::debug('Uploaded file', ['name' => $upload->getClientOriginalName(), 'size' => $upload->getSize(), 'mime' => $upload->getClientMimeType()]); - } - - // store configuration file's content into the job's configuration thing. Otherwise, leave it empty. - // demo user's configuration upload is ignored completely. - if ($request->files->has('configuration_file') && !auth()->user()->hasRole('demo')) { - /** @var UploadedFile $configFile */ - $configFile = $request->files->get('configuration_file'); - Log::debug( - 'Uploaded configuration file', - ['name' => $configFile->getClientOriginalName(), 'size' => $configFile->getSize(), 'mime' => $configFile->getClientMimeType()] - ); - - $configFileObject = new SplFileObject($configFile->getRealPath()); - $configRaw = $configFileObject->fread($configFileObject->getSize()); - $configuration = json_decode($configRaw, true); - - // @codeCoverageIgnoreStart - if (!is_null($configuration) && is_array($configuration)) { - Log::debug('Found configuration', $configuration); - $repository->setConfiguration($job, $configuration); - } - // @codeCoverageIgnoreEnd + Log::debug('Now in postConfigure()', ['job' => $job->key]); + $configurator = $this->makeConfigurator($job); + + // is the job already configured? + if ($configurator->isJobConfigured()) { + return redirect(route('import.status', [$job->key])); } + $data = $request->all(); + $configurator->configureJob($data); + // return to configure return redirect(route('import.configure', [$job->key])); } /** * @param ImportJob $job - * @param string $method * - * @return bool + * @return \Illuminate\Http\JsonResponse + * @throws FireflyException */ - private function jobInCorrectStep(ImportJob $job, string $method): bool + public function start(ImportJob $job) { - Log::debug('Now in jobInCorrectStep()', ['job' => $job->key, 'method' => $method]); - switch ($method) { - case 'configure': - case 'process': - return $job->status === 'import_status_never_started'; - case 'settings': - case 'store-settings': - Log::debug(sprintf('Job %d with key %s has status %s', $job->id, $job->key, $job->status)); - - return $job->status === 'import_configuration_saved'; - case 'finished': - return $job->status === 'import_complete'; - case 'complete': - return $job->status === 'settings_complete'; - case 'status': - return ($job->status === 'settings_complete') || ($job->status === 'import_running'); + /** @var ImportRoutine $routine */ + $routine = app(ImportRoutine::class); + $routine->setJob($job); + $result = $routine->run(); + if ($result) { + return Response::json(['run' => 'ok']); } - return false; // @codeCoverageIgnore - + throw new FireflyException('Job did not complete succesfully.'); } /** * @param ImportJob $job * - * @return SetupInterface - * @throws FireflyException + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View */ - private function makeImporter(ImportJob $job): SetupInterface + public function status(ImportJob $job) { - // create proper importer (depends on job) - $type = strtolower($job->file_type); - - // validate type: - $validTypes = array_keys(config('firefly.import_formats')); - - - if (in_array($type, $validTypes)) { - /** @var SetupInterface $importer */ - $importer = app('FireflyIII\Import\Setup\\' . ucfirst($type) . 'Setup'); - $importer->setJob($job); - - return $importer; + $statuses = ['configured', 'running', 'finished']; + if (!in_array($job->status, $statuses)) { + return redirect(route('import.configure', [$job->key])); } - throw new FireflyException(sprintf('"%s" is not a valid file type', $type)); // @codeCoverageIgnore + $subTitle = trans('firefly.import_status_sub_title'); + $subTitleIcon = 'fa-star'; + return view('import.status', compact('job', 'subTitle', 'subTitleIcon')); } /** * @param ImportJob $job * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @return ConfiguratorInterface * @throws FireflyException */ - private function redirectToCorrectStep(ImportJob $job) + private function makeConfigurator(ImportJob $job): ConfiguratorInterface { - Log::debug('Now in redirectToCorrectStep()', ['job' => $job->key]); - switch ($job->status) { - case 'import_status_never_started': - Log::debug('Will redirect to configure()'); - - return redirect(route('import.configure', [$job->key])); - case 'import_configuration_saved': - Log::debug('Will redirect to settings()'); - - return redirect(route('import.settings', [$job->key])); - case 'settings_complete': - Log::debug('Will redirect to complete()'); - - return redirect(route('import.complete', [$job->key])); - case 'import_complete': - Log::debug('Will redirect to finished()'); - - return redirect(route('import.finished', [$job->key])); + $type = $job->file_type; + $key = sprintf('firefly.import_configurators.%s', $type); + $className = config($key); + if (is_null($className)) { + throw new FireflyException('Cannot find configurator class for this job.'); // @codeCoverageIgnore } + /** @var ConfiguratorInterface $configurator */ + $configurator = app($className); + $configurator->setJob($job); - throw new FireflyException('Cannot redirect for job state ' . $job->status); // @codeCoverageIgnore + return $configurator; } } diff --git a/app/Http/Controllers/JavascriptController.php b/app/Http/Controllers/JavascriptController.php index e5e7fb0651..1ad10422c2 100644 --- a/app/Http/Controllers/JavascriptController.php +++ b/app/Http/Controllers/JavascriptController.php @@ -34,7 +34,7 @@ class JavascriptController extends Controller * @param AccountRepositoryInterface $repository * @param CurrencyRepositoryInterface $currencyRepository * - * @return $this + * @return \Illuminate\Http\Response */ public function accounts(AccountRepositoryInterface $repository, CurrencyRepositoryInterface $currencyRepository) { @@ -63,7 +63,7 @@ class JavascriptController extends Controller /** * @param CurrencyRepositoryInterface $repository * - * @return $this + * @return \Illuminate\Http\Response */ public function currencies(CurrencyRepositoryInterface $repository) { @@ -71,8 +71,8 @@ class JavascriptController extends Controller $data = ['currencies' => [],]; /** @var TransactionCurrency $currency */ foreach ($currencies as $currency) { - $currencyId = $currency->id; - $entry = ['name' => $currency->name, 'code' => $currency->code, 'symbol' => $currency->symbol]; + $currencyId = $currency->id; + $entry = ['name' => $currency->name, 'code' => $currency->code, 'symbol' => $currency->symbol]; $data['currencies'][$currencyId] = $entry; } diff --git a/app/Http/Controllers/Json/ExchangeController.php b/app/Http/Controllers/Json/ExchangeController.php index e96cae05e8..b1e3b5a60d 100644 --- a/app/Http/Controllers/Json/ExchangeController.php +++ b/app/Http/Controllers/Json/ExchangeController.php @@ -41,7 +41,6 @@ class ExchangeController extends Controller /** @var CurrencyRepositoryInterface $repository */ $repository = app(CurrencyRepositoryInterface::class); $rate = $repository->getExchangeRate($fromCurrency, $toCurrency, $date); - $amount = null; if (is_null($rate->id)) { Log::debug(sprintf('No cached exchange rate in database for %s to %s on %s', $fromCurrency->code, $toCurrency->code, $date->format('Y-m-d'))); $preferred = env('EXCHANGE_RATE_SERVICE', config('firefly.preferred_exchange_service')); @@ -63,4 +62,4 @@ class ExchangeController extends Controller return Response::json($return); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/JsonController.php b/app/Http/Controllers/JsonController.php index c8e916e231..378c5ebac2 100644 --- a/app/Http/Controllers/JsonController.php +++ b/app/Http/Controllers/JsonController.php @@ -20,7 +20,6 @@ use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; @@ -116,10 +115,11 @@ class JsonController extends Controller * Since both this method and the chart use the exact same data, we can suffice * with calling the one method in the bill repository that will get this amount. */ - $amount = $repository->getBillsPaidInRange($start, $end); // will be a negative amount. - $amount = bcmul($amount, '-1'); + $amount = $repository->getBillsPaidInRange($start, $end); // will be a negative amount. + $amount = bcmul($amount, '-1'); + $currency = Amount::getDefaultCurrency(); - $data = ['box' => 'bills-paid', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount]; + $data = ['box' => 'bills-paid', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; return Response::json($data); } @@ -131,19 +131,19 @@ class JsonController extends Controller */ public function boxBillsUnpaid(BillRepositoryInterface $repository) { - $start = session('start', Carbon::now()->startOfMonth()); - $end = session('end', Carbon::now()->endOfMonth()); - $amount = $repository->getBillsUnpaidInRange($start, $end); // will be a positive amount. - $data = ['box' => 'bills-unpaid', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount]; + $start = session('start', Carbon::now()->startOfMonth()); + $end = session('end', Carbon::now()->endOfMonth()); + $amount = $repository->getBillsUnpaidInRange($start, $end); // will be a positive amount. + $currency = Amount::getDefaultCurrency(); + $data = ['box' => 'bills-unpaid', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; return Response::json($data); } /** - * @param AccountTaskerInterface $accountTasker - * @param AccountRepositoryInterface $repository - * * @return \Illuminate\Http\JsonResponse + * @internal param AccountTaskerInterface $accountTasker + * @internal param AccountRepositoryInterface $repository * */ public function boxIn() @@ -167,18 +167,19 @@ class JsonController extends Controller ->setTypes([TransactionType::DEPOSIT]) ->withOpposingAccount(); - $amount = strval($collector->getJournals()->sum('transaction_amount')); - $data = ['box' => 'in', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount]; + $amount = strval($collector->getJournals()->sum('transaction_amount')); + $currency = Amount::getDefaultCurrency(); + $data = ['box' => 'in', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; $cache->store($data); return Response::json($data); } /** - * @param AccountTaskerInterface $accountTasker - * @param AccountRepositoryInterface $repository - * * @return \Symfony\Component\HttpFoundation\Response + * @internal param AccountTaskerInterface $accountTasker + * @internal param AccountRepositoryInterface $repository + * */ public function boxOut() { @@ -200,9 +201,9 @@ class JsonController extends Controller $collector->setAllAssetAccounts()->setRange($start, $end) ->setTypes([TransactionType::WITHDRAWAL]) ->withOpposingAccount(); - $amount = strval($collector->getJournals()->sum('transaction_amount')); - - $data = ['box' => 'out', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount]; + $amount = strval($collector->getJournals()->sum('transaction_amount')); + $currency = Amount::getDefaultCurrency(); + $data = ['box' => 'out', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; $cache->store($data); return Response::json($data); @@ -236,16 +237,6 @@ class JsonController extends Controller return Response::json($return); } - /** - * @return \Illuminate\Http\JsonResponse - */ - public function endTour() - { - Preferences::set('tour', false); - - return Response::json('true'); - } - /** * Returns a JSON list of all beneficiaries. * @@ -292,34 +283,6 @@ class JsonController extends Controller } - /** - * - */ - public function tour() - { - $pref = Preferences::get('tour', true); - if (!$pref) { - throw new FireflyException('Cannot find preference for tour. Exit.'); // @codeCoverageIgnore - } - $headers = ['main-content', 'sidebar-toggle', 'account-menu', 'budget-menu', 'report-menu', 'transaction-menu', 'option-menu', 'main-content-end']; - $steps = []; - foreach ($headers as $header) { - $steps[] = [ - 'element' => '#' . $header, - 'title' => trans('help.' . $header . '-title'), - 'content' => trans('help.' . $header . '-text'), - ]; - } - $steps[0]['orphan'] = true;// orphan and backdrop for first element. - $steps[0]['backdrop'] = true; - $steps[1]['placement'] = 'left';// sidebar position left: - $steps[7]['orphan'] = true; // final in the center again. - $steps[7]['backdrop'] = true; - $template = view('json.tour')->render(); - - return Response::json(['steps' => $steps, 'template' => $template]); - } - /** * @param JournalCollectorInterface $collector * @param string $what @@ -364,7 +327,7 @@ class JsonController extends Controller $keys = array_keys(config('firefly.rule-triggers')); $triggers = []; foreach ($keys as $key) { - if ($key != 'user_action') { + if ($key !== 'user_action') { $triggers[$key] = trans('firefly.rule_trigger_' . $key . '_choice'); } } diff --git a/app/Http/Controllers/NewUserController.php b/app/Http/Controllers/NewUserController.php index 478e5830d6..8e47d8a13b 100644 --- a/app/Http/Controllers/NewUserController.php +++ b/app/Http/Controllers/NewUserController.php @@ -54,7 +54,6 @@ class NewUserController extends Controller View::share('title', trans('firefly.welcome')); View::share('mainTitleIcon', 'fa-fire'); - $types = config('firefly.accountTypesByIdentifier.asset'); $count = $repository->count($types); @@ -74,30 +73,13 @@ class NewUserController extends Controller */ public function submit(NewUserFormRequest $request, AccountRepositoryInterface $repository) { - $count = 1; // create normal asset account: $this->createAssetAccount($request, $repository); // create savings account - $savingBalance = strval($request->get('savings_balance')) === '' ? '0' : strval($request->get('savings_balance')); - if (bccomp($savingBalance, '0') !== 0) { - $this->createSavingsAccount($request, $repository); - $count++; - } + $this->createSavingsAccount($request, $repository); - - // create credit card. - $limit = strval($request->get('credit_card_limit')) === '' ? '0' : strval($request->get('credit_card_limit')); - if (bccomp($limit, '0') !== 0) { - $this->storeCreditCard($request, $repository); - $count++; - } - $message = strval(trans('firefly.stored_new_accounts_new_user')); - if ($count == 1) { - $message = strval(trans('firefly.stored_new_account_new_user')); - } - - Session::flash('success', $message); + Session::flash('success', strval(trans('firefly.stored_new_accounts_new_user'))); Preferences::mark(); return redirect(route('index')); @@ -152,29 +134,4 @@ class NewUserController extends Controller return true; } - /** - * @param NewUserFormRequest $request - * @param AccountRepositoryInterface $repository - * - * @return bool - */ - private function storeCreditCard(NewUserFormRequest $request, AccountRepositoryInterface $repository): bool - { - $creditAccount = [ - 'name' => 'Credit card', - 'iban' => null, - 'accountType' => 'asset', - 'virtualBalance' => round($request->get('credit_card_limit'), 12), - 'active' => true, - 'accountRole' => 'ccAsset', - 'openingBalance' => null, - 'openingBalanceDate' => null, - 'openingBalanceCurrency' => intval($request->input('amount_currency_id_credit_card_limit')), - 'ccType' => 'monthlyFull', - 'ccMonthlyPaymentDate' => Carbon::now()->year . '-01-01', - ]; - $repository->store($creditAccount); - - return true; - } } diff --git a/app/Http/Controllers/PiggyBankController.php b/app/Http/Controllers/PiggyBankController.php index 052900cba5..207f98cd6f 100644 --- a/app/Http/Controllers/PiggyBankController.php +++ b/app/Http/Controllers/PiggyBankController.php @@ -276,18 +276,29 @@ class PiggyBankController extends Controller */ public function postAdd(Request $request, PiggyBankRepositoryInterface $repository, PiggyBank $piggyBank) { - $amount = $request->get('amount'); - + $amount = $request->get('amount'); + $currency = Amount::getDefaultCurrency(); if ($repository->canAddAmount($piggyBank, $amount)) { $repository->addAmount($piggyBank, $amount); - Session::flash('success', strval(trans('firefly.added_amount_to_piggy', ['amount' => Amount::format($amount, false), 'name' => $piggyBank->name]))); + Session::flash( + 'success', strval( + trans( + 'firefly.added_amount_to_piggy', + ['amount' => Amount::formatAnything($currency, $amount, false), 'name' => $piggyBank->name] + ) + ) + ); Preferences::mark(); return redirect(route('piggy-banks.index')); } Log::error('Cannot add ' . $amount . ' because canAddAmount returned false.'); - Session::flash('error', strval(trans('firefly.cannot_add_amount_piggy', ['amount' => Amount::format($amount, false), 'name' => e($piggyBank->name)]))); + Session::flash( + 'error', strval( + trans('firefly.cannot_add_amount_piggy', ['amount' => Amount::formatAnything($currency, $amount, false), 'name' => e($piggyBank->name)]) + ) + ); return redirect(route('piggy-banks.index')); } @@ -301,11 +312,13 @@ class PiggyBankController extends Controller */ public function postRemove(Request $request, PiggyBankRepositoryInterface $repository, PiggyBank $piggyBank) { - $amount = $request->get('amount'); + $amount = $request->get('amount'); + $currency = Amount::getDefaultCurrency(); if ($repository->canRemoveAmount($piggyBank, $amount)) { $repository->removeAmount($piggyBank, $amount); Session::flash( - 'success', strval(trans('firefly.removed_amount_from_piggy', ['amount' => Amount::format($amount, false), 'name' => $piggyBank->name])) + 'success', + strval(trans('firefly.removed_amount_from_piggy', ['amount' => Amount::formatAnything($currency, $amount, false), 'name' => $piggyBank->name])) ); Preferences::mark(); @@ -314,7 +327,11 @@ class PiggyBankController extends Controller $amount = strval(round($request->get('amount'), 12)); - Session::flash('error', strval(trans('firefly.cannot_remove_from_piggy', ['amount' => Amount::format($amount, false), 'name' => e($piggyBank->name)]))); + Session::flash( + 'error', strval( + trans('firefly.cannot_remove_from_piggy', ['amount' => Amount::formatAnything($currency, $amount, false), 'name' => e($piggyBank->name)]) + ) + ); return redirect(route('piggy-banks.index')); } @@ -380,7 +397,7 @@ class PiggyBankController extends Controller // @codeCoverageIgnoreEnd } - return redirect($this->getPreviousUri('piggy-banks.edit.uri')); + return redirect($this->getPreviousUri('piggy-banks.create.uri')); } /** diff --git a/app/Http/Controllers/Report/CategoryController.php b/app/Http/Controllers/Report/CategoryController.php index 1528f44460..250e54dde9 100644 --- a/app/Http/Controllers/Report/CategoryController.php +++ b/app/Http/Controllers/Report/CategoryController.php @@ -120,7 +120,7 @@ class CategoryController extends Controller foreach ($categories as $category) { $spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $start, $end); if (bccomp($spent, '0') !== 0) { - $report[$category->id] = ['name' => $category->name, 'spent' => $spent]; + $report[$category->id] = ['name' => $category->name, 'spent' => $spent, 'id' => $category->id]; } } diff --git a/app/Http/Controllers/Report/OperationsController.php b/app/Http/Controllers/Report/OperationsController.php index 5081ba6554..59643b578d 100644 --- a/app/Http/Controllers/Report/OperationsController.php +++ b/app/Http/Controllers/Report/OperationsController.php @@ -15,10 +15,7 @@ namespace FireflyIII\Http\Controllers\Report; use Carbon\Carbon; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Models\Transaction; -use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; diff --git a/app/Http/Controllers/RuleController.php b/app/Http/Controllers/RuleController.php index a7247b7705..5ab2f5e4ae 100644 --- a/app/Http/Controllers/RuleController.php +++ b/app/Http/Controllers/RuleController.php @@ -282,7 +282,7 @@ class RuleController extends Controller // build trigger array from response $triggers = $this->getValidTriggerList($request); - if (count($triggers) == 0) { + if (count($triggers) === 0) { return Response::json(['html' => '', 'warning' => trans('firefly.warning_no_valid_triggers')]); } @@ -298,15 +298,15 @@ class RuleController extends Controller // Warn the user if only a subset of transactions is returned $warning = ''; - if (count($matchingTransactions) == $limit) { + if (count($matchingTransactions) === $limit) { $warning = trans('firefly.warning_transaction_subset', ['max_num_transactions' => $limit]); } - if (count($matchingTransactions) == 0) { + if (count($matchingTransactions) === 0) { $warning = trans('firefly.warning_no_matching_transactions', ['num_transactions' => $range]); } // Return json response - $view = view('list.journals-tiny-tasker', ['transactions' => $matchingTransactions])->render(); + $view = view('list.journals-tiny', ['transactions' => $matchingTransactions])->render(); return Response::json(['html' => $view, 'warning' => $warning]); } @@ -440,7 +440,7 @@ class RuleController extends Controller /** @var RuleTrigger $entry */ foreach ($rule->ruleTriggers as $entry) { - if ($entry->trigger_type != 'user_action') { + if ($entry->trigger_type !== 'user_action') { $count = ($index + 1); $triggers[] = view( 'rules.partials.trigger', diff --git a/app/Http/Controllers/RuleGroupController.php b/app/Http/Controllers/RuleGroupController.php index 12f9a0bb58..d0f60c08d3 100644 --- a/app/Http/Controllers/RuleGroupController.php +++ b/app/Http/Controllers/RuleGroupController.php @@ -253,7 +253,7 @@ class RuleGroupController extends Controller $data = [ 'title' => $request->input('title'), 'description' => $request->input('description'), - 'active' => intval($request->input('active')) == 1, + 'active' => intval($request->input('active')) === 1, ]; $repository->update($ruleGroup, $data); diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index f014f8325a..a0acba5a7e 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -16,6 +16,7 @@ namespace FireflyIII\Http\Controllers; use FireflyIII\Support\Search\SearchInterface; use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Response; use View; /** @@ -51,6 +52,15 @@ class SearchController extends Controller */ public function index(Request $request, SearchInterface $searcher) { + $fullQuery = $request->get('q'); + + // parse search terms: + $searcher->parseQuery($fullQuery); + $query = $searcher->getWordsAsString(); + $subTitle = trans('breadcrumbs.search_result', ['query' => $query]); + + return view('search.index', compact('query', 'fullQuery', 'subTitle')); + // yes, hard coded values: $minSearchLen = 1; $limit = 20; @@ -94,4 +104,19 @@ class SearchController extends Controller return view('search.index', compact('rawQuery', 'hasModifiers', 'modifiers', 'subTitle', 'limit', 'query', 'result')); } + public function search(Request $request, SearchInterface $searcher) + { + $fullQuery = $request->get('query'); + + // parse search terms: + $searcher->parseQuery($fullQuery); + $searcher->setLimit(20); + $transactions = $searcher->searchTransactions(); + $html = view('search.search', compact('transactions'))->render(); + + return Response::json(['count' => $transactions->count(), 'html' => $html]); + + + } + } diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index fa94a1b612..f373fa7dff 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -15,6 +15,7 @@ namespace FireflyIII\Http\Controllers; use Carbon\Carbon; use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Http\Requests\TagFormRequest; use FireflyIII\Models\Tag; use FireflyIII\Repositories\Tag\TagRepositoryInterface; @@ -235,7 +236,7 @@ class TagController extends Controller // default values: $subTitle = $tag->tag; $subTitleIcon = 'fa-tag'; - $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $count = 0; $loop = 0; @@ -245,6 +246,7 @@ class TagController extends Controller $periods = new Collection; $apiKey = env('GOOGLE_MAPS_API_KEY', ''); $sum = '0'; + $path = 'tags/show/' . $tag->id; // prep for "all" view. @@ -253,6 +255,7 @@ class TagController extends Controller $start = $repository->firstUseDate($tag); $end = new Carbon; $sum = $repository->sumOfTag($tag); + $path = 'tags/show/' . $tag->id . '/all'; } // prep for "specific date" view. @@ -282,15 +285,15 @@ class TagController extends Controller Log::info('Now at tag loop start.'); while ($count === 0 && $loop < 3) { $loop++; - Log::info('Count is zero, search for journals.'); + Log::info(sprintf('Count is zero, search for journals between %s and %s (pagesize %d, page %d).', $start, $end, $pageSize, $page)); /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class); $collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount() - ->setTag($tag)->withBudgetInformation()->withCategoryInformation(); + ->setTag($tag)->withBudgetInformation()->withCategoryInformation()->removeFilter(InternalTransferFilter::class); $journals = $collector->getPaginatedJournals(); - $journals->setPath('tags/show/' . $tag->id); + $journals->setPath($path); $count = $journals->getCollection()->count(); - if ($count === 0) { + if ($count === 0 && $loop < 3) { $start->subDay(); $start = Navigation::startOfPeriod($start, $range); $end = Navigation::endOfPeriod($start, $range); @@ -298,7 +301,7 @@ class TagController extends Controller } } - if ($moment != 'all' && $loop > 1) { + if ($moment !== 'all' && $loop > 1) { $subTitle = trans( 'firefly.journals_in_period_for_tag', ['tag' => $tag->tag, 'start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] diff --git a/app/Http/Controllers/Transaction/ConvertController.php b/app/Http/Controllers/Transaction/ConvertController.php index 8853a3dbaa..b01335dce7 100644 --- a/app/Http/Controllers/Transaction/ConvertController.php +++ b/app/Http/Controllers/Transaction/ConvertController.php @@ -174,14 +174,17 @@ class ConvertController extends Controller switch ($joined) { default: throw new FireflyException('Cannot handle ' . $joined); // @codeCoverageIgnore - case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: // one + case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: + // one $destination = $sourceAccount; break; - case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: // two + case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: + // two $destination = $accountRepository->find(intval($data['destination_account_asset'])); break; - case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: // three - case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: // five + case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: + case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: + // three and five if ($data['destination_account_expense'] === '') { // destination is a cash account. $destination = $accountRepository->getCashAccount(); @@ -197,8 +200,9 @@ class ConvertController extends Controller ]; $destination = $accountRepository->store($data); break; - case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: // four - case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: // six + case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: + case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: + // four and six $destination = $destinationAccount; break; } @@ -225,8 +229,8 @@ class ConvertController extends Controller switch ($joined) { default: throw new FireflyException('Cannot handle ' . $joined); // @codeCoverageIgnore - case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: // one - case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: // six + case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: + case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: if ($data['source_account_revenue'] === '') { // destination is a cash account. @@ -244,14 +248,14 @@ class ConvertController extends Controller ]; $source = $accountRepository->store($data); break; - case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: // two - case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: // five + case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: + case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: $source = $sourceAccount; break; - case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: // three + case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: $source = $destinationAccount; break; - case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: // four + case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: $source = $accountRepository->find(intval($data['source_account_asset'])); break; } diff --git a/app/Http/Controllers/Transaction/MassController.php b/app/Http/Controllers/Transaction/MassController.php index 285a73f3e9..fe5c99c4b3 100644 --- a/app/Http/Controllers/Transaction/MassController.php +++ b/app/Http/Controllers/Transaction/MassController.php @@ -19,6 +19,7 @@ use FireflyIII\Http\Requests\MassDeleteJournalRequest; use FireflyIII\Http\Requests\MassEditJournalRequest; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; @@ -85,7 +86,7 @@ class MassController extends Controller foreach ($ids as $journalId) { /** @var TransactionJournal $journal */ $journal = $repository->find(intval($journalId)); - if (!is_null($journal->id) && $journalId == $journal->id) { + if (!is_null($journal->id) && $journalId === $journal->id) { $set->push($journal); } } @@ -126,8 +127,7 @@ class MassController extends Controller $budgetRepository = app(BudgetRepositoryInterface::class); $budgets = $budgetRepository->getBudgets(); - // skip transactions that have multiple destinations - // or multiple sources: + // skip transactions that have multiple destinations, multiple sources or are an opening balance. $filtered = new Collection; $messages = []; /** @@ -146,6 +146,10 @@ class MassController extends Controller $messages[] = trans('firefly.cannot_edit_multiple_dest', ['description' => $journal->description, 'id' => $journal->id]); continue; } + if ($journal->transactionType->type === TransactionType::OPENING_BALANCE) { + $messages[] = trans('firefly.cannot_edit_opening_balance'); + continue; + } $filtered->push($journal); } @@ -158,13 +162,21 @@ class MassController extends Controller Session::flash('gaEventCategory', 'transactions'); Session::flash('gaEventAction', 'mass-edit'); - // set some values to be used in the edit routine: + // collect some useful meta data for the mass edit: $filtered->each( function (TransactionJournal $journal) { - $journal->amount = $journal->amountPositive(); - $sources = $journal->sourceAccountList(); - $destinations = $journal->destinationAccountList(); - $journal->transaction_count = $journal->transactions()->count(); + $transaction = $journal->positiveTransaction(); + $currency = $transaction->transactionCurrency; + $journal->amount = floatval($transaction->amount); + $sources = $journal->sourceAccountList(); + $destinations = $journal->destinationAccountList(); + $journal->transaction_count = $journal->transactions()->count(); + $journal->currency_symbol = $currency->symbol; + $journal->transaction_type_type = $journal->transactionType->type; + + $journal->foreign_amount = floatval($transaction->foreign_amount); + $journal->foreign_currency = $transaction->foreignCurrency; + if (!is_null($sources->first())) { $journal->source_account_id = $sources->first()->id; $journal->source_account_name = $sources->first()->editname; @@ -208,6 +220,10 @@ class MassController extends Controller $budgetId = $request->get('budget_id')[$journal->id] ?? 0; $category = $request->get('category')[$journal->id]; $tags = $journal->tags->pluck('tag')->toArray(); + $amount = round($request->get('amount')[$journal->id], 12); + $foreignAmount = isset($request->get('foreign_amount')[$journal->id]) ? round($request->get('foreign_amount')[$journal->id], 12) : null; + $foreignCurrencyId = isset($request->get('foreign_currency_id')[$journal->id]) ? + intval($request->get('foreign_currency_id')[$journal->id]) : null; // build data array $data = [ @@ -218,16 +234,19 @@ class MassController extends Controller 'source_account_name' => $sourceAccountName, 'destination_account_id' => intval($destAccountId), 'destination_account_name' => $destAccountName, - 'amount' => round($request->get('amount')[$journal->id], 12), - 'currency_id' => $journal->transaction_currency_id, + 'amount' => $foreignAmount, + 'native_amount' => $amount, + 'source_amount' => $amount, 'date' => new Carbon($request->get('date')[$journal->id]), 'interest_date' => $journal->interest_date, 'book_date' => $journal->book_date, 'process_date' => $journal->process_date, 'budget_id' => intval($budgetId), + 'currency_id' => $foreignCurrencyId, + 'foreign_amount' => $foreignAmount, + 'destination_amount' => $foreignAmount, 'category' => $category, 'tags' => $tags, - ]; // call repository update function. $repository->update($journal, $data); @@ -235,6 +254,7 @@ class MassController extends Controller $count++; } } + } Preferences::mark(); Session::flash('success', trans('firefly.mass_edited_transactions_success', ['amount' => $count])); diff --git a/app/Http/Controllers/Transaction/SingleController.php b/app/Http/Controllers/Transaction/SingleController.php index 27c7cdba77..6b1a138ee5 100644 --- a/app/Http/Controllers/Transaction/SingleController.php +++ b/app/Http/Controllers/Transaction/SingleController.php @@ -21,6 +21,7 @@ use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\JournalFormRequest; use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; @@ -93,27 +94,35 @@ class SingleController extends Controller $category = $journal->categories()->first(); $categoryName = is_null($category) ? '' : $category->name; $tags = join(',', $journal->tags()->get()->pluck('tag')->toArray()); - + /** @var Transaction $transaction */ + $transaction = $journal->transactions()->first(); + $amount = Steam::positive($transaction->amount); + $foreignAmount = is_null($transaction->foreign_amount) ? null : Steam::positive($transaction->foreign_amount); $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' => $journal->amountPositive(), - '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'), + 'description' => $journal->description, + 'source_account_id' => $source->id, + 'source_account_name' => $source->name, + 'destination_account_id' => $destination->id, + 'destination_account_name' => $destination->name, + 'amount' => $amount, + 'source_amount' => $amount, + 'destination_amount' => $foreignAmount, + 'foreign_amount' => $foreignAmount, + 'native_amount' => $foreignAmount, + 'amount_currency_id_amount' => $transaction->foreign_currency_id ?? 0, + '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); @@ -238,6 +247,7 @@ class SingleController extends Controller $sourceAccounts = $journal->sourceAccountList(); $destinationAccounts = $journal->destinationAccountList(); $optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data; + $pTransaction = $journal->positiveTransaction(); $preFilled = [ 'date' => $journal->dateAsString(), 'interest_date' => $journal->dateAsString('interest_date'), @@ -250,8 +260,6 @@ class SingleController extends Controller 'source_account_name' => $sourceAccounts->first()->edit_name, 'destination_account_id' => $destinationAccounts->first()->id, 'destination_account_name' => $destinationAccounts->first()->edit_name, - 'amount' => $journal->amountPositive(), - 'currency' => $journal->transactionCurrency, // new custom fields: 'due_date' => $journal->dateAsString('due_date'), @@ -260,26 +268,36 @@ class SingleController extends Controller 'interal_reference' => $journal->getMeta('internal_reference'), 'notes' => $journal->getMeta('notes'), - // exchange rate fields - 'native_amount' => $journal->amountPositive(), - 'native_currency' => $journal->transactionCurrency, + // amount fields + 'amount' => $pTransaction->amount, + 'source_amount' => $pTransaction->amount, + 'native_amount' => $pTransaction->amount, + 'destination_amount' => $pTransaction->foreign_amount, + 'currency' => $pTransaction->transactionCurrency, + 'source_currency' => $pTransaction->transactionCurrency, + 'native_currency' => $pTransaction->transactionCurrency, + 'foreign_currency' => !is_null($pTransaction->foreignCurrency) ? $pTransaction->foreignCurrency : $pTransaction->transactionCurrency, + 'destination_currency' => !is_null($pTransaction->foreignCurrency) ? $pTransaction->foreignCurrency : $pTransaction->transactionCurrency, ]; - // if user has entered a foreign currency, update some fields - $foreignCurrencyId = intval($journal->getMeta('foreign_currency_id')); - if ($foreignCurrencyId > 0) { - // update some fields in pre-filled. - // @codeCoverageIgnoreStart - $preFilled['amount'] = $journal->getMeta('foreign_amount'); - $preFilled['currency'] = $this->currency->find(intval($journal->getMeta('foreign_currency_id'))); - // @codeCoverageIgnoreEnd + // amounts for withdrawals and deposits: + // amount, native_amount, source_amount, destination_amount + if (($journal->isWithdrawal() || $journal->isDeposit()) && !is_null($pTransaction->foreign_amount)) { + $preFilled['amount'] = $pTransaction->foreign_amount; + $preFilled['currency'] = $pTransaction->foreignCurrency; } - if ($journal->isWithdrawal() && $destinationAccounts->first()->accountType->type == AccountType::CASH) { + if ($journal->isTransfer() && !is_null($pTransaction->foreign_amount)) { + $preFilled['destination_amount'] = $pTransaction->foreign_amount; + $preFilled['destination_currency'] = $pTransaction->foreignCurrency; + } + + // fixes for cash accounts: + if ($journal->isWithdrawal() && $destinationAccounts->first()->accountType->type === AccountType::CASH) { $preFilled['destination_account_name'] = ''; } - if ($journal->isDeposit() && $sourceAccounts->first()->accountType->type == AccountType::CASH) { + if ($journal->isDeposit() && $sourceAccounts->first()->accountType->type === AccountType::CASH) { $preFilled['source_account_name'] = ''; } @@ -319,6 +337,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); diff --git a/app/Http/Controllers/Transaction/SplitController.php b/app/Http/Controllers/Transaction/SplitController.php index c587129811..32f0807c3b 100644 --- a/app/Http/Controllers/Transaction/SplitController.php +++ b/app/Http/Controllers/Transaction/SplitController.php @@ -93,7 +93,7 @@ class SplitController extends Controller } $uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size'))); - $currencies = ExpandedForm::makeSelectList($this->currencies->get()); + $currencies = $this->currencies->get(); $assetAccounts = ExpandedForm::makeSelectList($this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])); $optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data; $budgets = ExpandedForm::makeSelectListWithEmpty($this->budgets->getActiveBudgets()); @@ -130,7 +130,6 @@ class SplitController extends Controller */ public function update(Request $request, JournalRepositoryInterface $repository, TransactionJournal $journal) { - if ($this->isOpeningBalance($journal)) { return $this->redirectToAccount($journal); } @@ -179,7 +178,6 @@ class SplitController extends Controller 'journal_source_account_id' => $request->get('journal_source_account_id'), 'journal_source_account_name' => $request->get('journal_source_account_name'), 'journal_destination_account_id' => $request->get('journal_destination_account_id'), - 'currency_id' => $request->get('currency_id'), 'what' => $request->get('what'), 'date' => $request->get('date'), // all custom fields: @@ -218,10 +216,9 @@ class SplitController extends Controller 'journal_source_account_id' => $request->old('journal_source_account_id', $sourceAccounts->first()->id), 'journal_source_account_name' => $request->old('journal_source_account_name', $sourceAccounts->first()->name), 'journal_destination_account_id' => $request->old('journal_destination_account_id', $destinationAccounts->first()->id), - 'currency_id' => $request->old('currency_id', $journal->transaction_currency_id), 'destinationAccounts' => $destinationAccounts, 'what' => strtolower($journal->transactionTypeStr()), - 'date' => $request->old('date', $journal->date), + 'date' => $request->old('date', $journal->date->format('Y-m-d')), 'tags' => join(',', $journal->tags->pluck('tag')->toArray()), // all custom fields: @@ -253,14 +250,22 @@ class SplitController extends Controller /** @var array $transaction */ foreach ($transactions as $index => $transaction) { $set = [ - 'description' => $transaction['description'], - 'source_account_id' => $transaction['source_account_id'], - 'source_account_name' => $transaction['source_account_name'], - 'destination_account_id' => $transaction['destination_account_id'], - 'destination_account_name' => $transaction['destination_account_name'], - 'amount' => round($transaction['destination_amount'], 12), - 'budget_id' => isset($transaction['budget_id']) ? intval($transaction['budget_id']) : 0, - 'category' => $transaction['category'], + 'description' => $transaction['description'], + 'source_account_id' => $transaction['source_account_id'], + 'source_account_name' => $transaction['source_account_name'], + 'destination_account_id' => $transaction['destination_account_id'], + 'destination_account_name' => $transaction['destination_account_name'], + 'amount' => round($transaction['destination_amount'], 12), + 'budget_id' => isset($transaction['budget_id']) ? intval($transaction['budget_id']) : 0, + 'category' => $transaction['category'], + 'transaction_currency_id' => $transaction['transaction_currency_id'], + 'transaction_currency_code' => $transaction['transaction_currency_code'], + 'transaction_currency_symbol' => $transaction['transaction_currency_symbol'], + 'foreign_amount' => round($transaction['foreign_destination_amount'], 12), + 'foreign_currency_id' => $transaction['foreign_currency_id'], + 'foreign_currency_code' => $transaction['foreign_currency_code'], + 'foreign_currency_symbol' => $transaction['foreign_currency_symbol'], + ]; // set initial category and/or budget: @@ -294,8 +299,12 @@ class SplitController extends Controller 'destination_account_id' => $transaction['destination_account_id'] ?? 0, 'destination_account_name' => $transaction['destination_account_name'] ?? '', 'amount' => round($transaction['amount'] ?? 0, 12), + 'foreign_amount' => !isset($transaction['foreign_amount']) ? null : round($transaction['foreign_amount'] ?? 0, 12), 'budget_id' => isset($transaction['budget_id']) ? intval($transaction['budget_id']) : 0, 'category' => $transaction['category'] ?? '', + 'transaction_currency_id' => intval($transaction['transaction_currency_id']), + 'foreign_currency_id' => $transaction['foreign_currency_id'] ?? null, + ]; } Log::debug(sprintf('Found %d splits in request data.', count($return))); diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index 1e838c5dec..e506a6fa57 100644 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -18,7 +18,6 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Models\TransactionJournal; -use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Journal\JournalTaskerInterface; use FireflyIII\Support\CacheProperties; @@ -71,7 +70,7 @@ class TransactionController extends Controller // default values: $subTitleIcon = config('firefly.transactionIconsByWhat.' . $what); $types = config('firefly.transactionTypesByWhat.' . $what); - $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $count = 0; $loop = 0; @@ -79,6 +78,7 @@ class TransactionController extends Controller $start = null; $end = null; $periods = new Collection; + $path = '/transactions/' . $what; // prep for "all" view. if ($moment === 'all') { @@ -86,12 +86,14 @@ class TransactionController extends Controller $first = $repository->first(); $start = $first->date ?? new Carbon; $end = new Carbon; + $path = '/transactions/' . $what . '/all/'; } // prep for "specific date" view. if (strlen($moment) > 0 && $moment !== 'all') { $start = new Carbon($moment); $end = Navigation::endOfPeriod($start, $range); + $path = '/transactions/' . $what . '/' . $moment; $subTitle = trans( 'firefly.title_' . $what . '_between', ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] @@ -119,9 +121,9 @@ class TransactionController extends Controller $collector->setAllAssetAccounts()->setRange($start, $end)->setTypes($types)->setLimit($pageSize)->setPage($page)->withOpposingAccount(); $collector->removeFilter(InternalTransferFilter::class); $journals = $collector->getPaginatedJournals(); - $journals->setPath('/transactions/' . $what); + $journals->setPath($path); $count = $journals->getCollection()->count(); - if ($count === 0) { + if ($count === 0 && $loop < 3) { $start->subDay(); $start = Navigation::startOfPeriod($start, $range); $end = Navigation::endOfPeriod($start, $range); @@ -129,7 +131,7 @@ class TransactionController extends Controller } } - if ($moment != 'all' && $loop > 1) { + if ($moment !== 'all' && $loop > 1) { $subTitle = trans( 'firefly.title_' . $what . '_between', ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] @@ -179,21 +181,12 @@ class TransactionController extends Controller return $this->redirectToAccount($journal); } - $events = $tasker->getPiggyBankEvents($journal); - $transactions = $tasker->getTransactionsOverview($journal); - $what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); - $subTitle = trans('firefly.' . $what) . ' "' . e($journal->description) . '"'; - $foreignCurrency = null; + $events = $tasker->getPiggyBankEvents($journal); + $transactions = $tasker->getTransactionsOverview($journal); + $what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); + $subTitle = trans('firefly.' . $what) . ' "' . e($journal->description) . '"'; - if ($journal->hasMeta('foreign_currency_id')) { - // @codeCoverageIgnoreStart - /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class); - $foreignCurrency = $repository->find(intval($journal->getMeta('foreign_currency_id'))); - // @codeCoverageIgnoreEnd - } - - return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions', 'foreignCurrency')); + return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions')); } diff --git a/app/Http/Requests/AccountFormRequest.php b/app/Http/Requests/AccountFormRequest.php index 90b8eb760d..bf24b65418 100644 --- a/app/Http/Requests/AccountFormRequest.php +++ b/app/Http/Requests/AccountFormRequest.php @@ -38,19 +38,19 @@ class AccountFormRequest extends Request public function getAccountData(): array { return [ - 'name' => $this->string('name'), - 'active' => $this->boolean('active'), - 'accountType' => $this->string('what'), - 'currency_id' => $this->integer('currency_id'), - 'virtualBalance' => $this->float('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'), - 'ccType' => $this->string('ccType'), - 'ccMonthlyPaymentDate' => $this->string('ccMonthlyPaymentDate'), + 'name' => $this->string('name'), + 'active' => $this->boolean('active'), + 'accountType' => $this->string('what'), + 'currency_id' => $this->integer('currency_id'), + 'virtualBalance' => $this->float('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'), + 'ccType' => $this->string('ccType'), + 'ccMonthlyPaymentDate' => $this->string('ccMonthlyPaymentDate'), ]; } diff --git a/app/Http/Requests/ImportUploadRequest.php b/app/Http/Requests/ImportUploadRequest.php index 93065bcada..758ee6164a 100644 --- a/app/Http/Requests/ImportUploadRequest.php +++ b/app/Http/Requests/ImportUploadRequest.php @@ -38,8 +38,9 @@ class ImportUploadRequest extends Request $types = array_keys(config('firefly.import_formats')); return [ - 'import_file' => 'required|file', - 'import_file_type' => 'required|in:' . join(',', $types), + 'import_file' => 'required|file', + 'import_file_type' => 'required|in:' . join(',', $types), + 'configuration_file' => 'file', ]; } diff --git a/app/Http/Requests/TagFormRequest.php b/app/Http/Requests/TagFormRequest.php index d61e5cb8d9..1348a0d767 100644 --- a/app/Http/Requests/TagFormRequest.php +++ b/app/Http/Requests/TagFormRequest.php @@ -37,7 +37,7 @@ class TagFormRequest extends Request */ public function collectTagData(): array { - if ($this->get('setTag') == 'true') { + if ($this->get('setTag') === 'true') { $latitude = $this->string('latitude'); $longitude = $this->string('longitude'); $zoomLevel = $this->integer('zoomLevel'); diff --git a/app/Http/breadcrumbs.php b/app/Http/breadcrumbs.php index 1b98afd9b4..df835e940a 100644 --- a/app/Http/breadcrumbs.php +++ b/app/Http/breadcrumbs.php @@ -78,10 +78,10 @@ Breadcrumbs::register( $breadcrumbs->push(trans('firefly.everything'), route('accounts.show', [$account->id, 'all'])); } // when is specific period or when empty: - if ($moment !== 'all') { + if ($moment !== 'all' && $moment !== '(nothing)') { $title = trans( 'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))), - 'end' => $end->formatLocalized(strval(trans('config.month_and_day')))] + 'end' => $end->formatLocalized(strval(trans('config.month_and_day')))] ); $breadcrumbs->push($title, route('accounts.show', [$account->id, $moment, $start, $end])); } @@ -91,7 +91,7 @@ Breadcrumbs::register( Breadcrumbs::register( 'accounts.delete', function (BreadCrumbGenerator $breadcrumbs, Account $account) { - $breadcrumbs->parent('accounts.show', $account, '', new Carbon, new Carbon); + $breadcrumbs->parent('accounts.show', $account, '(nothing)', new Carbon, new Carbon); $breadcrumbs->push(trans('firefly.delete_account', ['name' => e($account->name)]), route('accounts.delete', [$account->id])); } ); @@ -99,7 +99,7 @@ Breadcrumbs::register( Breadcrumbs::register( 'accounts.edit', function (BreadCrumbGenerator $breadcrumbs, Account $account) { - $breadcrumbs->parent('accounts.show', $account, '', new Carbon, new Carbon); + $breadcrumbs->parent('accounts.show', $account, '(nothing)', new Carbon, new Carbon); $what = config('firefly.shortNamesByFullName.' . $account->accountType->type); $breadcrumbs->push(trans('firefly.edit_' . $what . '_account', ['name' => e($account->name)]), route('accounts.edit', [$account->id])); @@ -257,8 +257,8 @@ Breadcrumbs::register( if ($moment === 'all') { $breadcrumbs->push(trans('firefly.everything'), route('budgets.no-budget', ['all'])); } - // when is specific period: - if ($moment !== 'all') { + // when is specific period or when empty: + if ($moment !== 'all' && $moment !== '(nothing)') { $title = trans( 'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))), 'end' => $end->formatLocalized(strval(trans('config.month_and_day')))] @@ -312,13 +312,13 @@ Breadcrumbs::register( Breadcrumbs::register( 'categories.edit', function (BreadCrumbGenerator $breadcrumbs, Category $category) { - $breadcrumbs->parent('categories.show', $category, '', new Carbon, new Carbon); + $breadcrumbs->parent('categories.show', $category, '(nothing)', new Carbon, new Carbon); $breadcrumbs->push(trans('firefly.edit_category', ['name' => e($category->name)]), route('categories.edit', [$category->id])); } ); Breadcrumbs::register( 'categories.delete', function (BreadCrumbGenerator $breadcrumbs, Category $category) { - $breadcrumbs->parent('categories.show', $category, '', new Carbon, new Carbon); + $breadcrumbs->parent('categories.show', $category, '(nothing)', new Carbon, new Carbon); $breadcrumbs->push(trans('firefly.delete_category', ['name' => e($category->name)]), route('categories.delete', [$category->id])); } ); @@ -333,8 +333,8 @@ Breadcrumbs::register( if ($moment === 'all') { $breadcrumbs->push(trans('firefly.everything'), route('categories.show', [$category->id, 'all'])); } - // when is specific period: - if ($moment !== 'all') { + // when is specific period or when empty: + if ($moment !== 'all' && $moment !== '(nothing)') { $title = trans( 'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))), 'end' => $end->formatLocalized(strval(trans('config.month_and_day')))] @@ -354,8 +354,8 @@ Breadcrumbs::register( if ($moment === 'all') { $breadcrumbs->push(trans('firefly.everything'), route('categories.no-category', ['all'])); } - // when is specific period: - if ($moment !== 'all') { + // when is specific period or when empty: + if ($moment !== 'all' && $moment !== '(nothing)') { $title = trans( 'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))), 'end' => $end->formatLocalized(strval(trans('config.month_and_day')))] @@ -469,26 +469,20 @@ Breadcrumbs::register( $breadcrumbs->push(trans('firefly.import'), route('import.index')); } ); -Breadcrumbs::register( - 'import.complete', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) { - $breadcrumbs->parent('import.index'); - $breadcrumbs->push(trans('firefly.bread_crumb_import_complete', ['key' => $job->key]), route('import.complete', [$job->key])); -} -); + Breadcrumbs::register( 'import.configure', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) { $breadcrumbs->parent('import.index'); - $breadcrumbs->push(trans('firefly.bread_crumb_configure_import', ['key' => $job->key]), route('import.configure', [$job->key])); + $breadcrumbs->push(trans('firefly.import_config_sub_title', ['key' => $job->key]), route('import.configure', [$job->key])); } ); Breadcrumbs::register( - 'import.finished', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) { + 'import.status', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) { $breadcrumbs->parent('import.index'); - $breadcrumbs->push(trans('firefly.bread_crumb_import_finished', ['key' => $job->key]), route('import.finished', [$job->key])); + $breadcrumbs->push(trans('firefly.import_status_bread_crumb', ['key' => $job->key]), route('import.status', [$job->key])); } ); - /** * PREFERENCES */ @@ -686,7 +680,7 @@ Breadcrumbs::register( Breadcrumbs::register( 'search.index', function (BreadCrumbGenerator $breadcrumbs, $query) { $breadcrumbs->parent('home'); - $breadcrumbs->push(trans('breadcrumbs.searchResult', ['query' => e($query)]), route('search.index')); + $breadcrumbs->push(trans('breadcrumbs.search_result', ['query' => e($query)]), route('search.index')); } ); @@ -710,14 +704,14 @@ Breadcrumbs::register( Breadcrumbs::register( 'tags.edit', function (BreadCrumbGenerator $breadcrumbs, Tag $tag) { - $breadcrumbs->parent('tags.show', $tag, '', new Carbon, new Carbon); + $breadcrumbs->parent('tags.show', $tag, '(nothing)', new Carbon, new Carbon); $breadcrumbs->push(trans('breadcrumbs.edit_tag', ['tag' => e($tag->tag)]), route('tags.edit', [$tag->id])); } ); Breadcrumbs::register( 'tags.delete', function (BreadCrumbGenerator $breadcrumbs, Tag $tag) { - $breadcrumbs->parent('tags.show', $tag, '', new Carbon, new Carbon); + $breadcrumbs->parent('tags.show', $tag, '(nothing)', new Carbon, new Carbon); $breadcrumbs->push(trans('breadcrumbs.delete_tag', ['tag' => e($tag->tag)]), route('tags.delete', [$tag->id])); } ); @@ -726,16 +720,17 @@ Breadcrumbs::register( Breadcrumbs::register( 'tags.show', function (BreadCrumbGenerator $breadcrumbs, Tag $tag, string $moment, Carbon $start, Carbon $end) { $breadcrumbs->parent('tags.index'); - $breadcrumbs->push(e($tag->tag), route('tags.show', [$tag->id], $moment)); + $breadcrumbs->push(e($tag->tag), route('tags.show', [$tag->id, $moment])); if ($moment === 'all') { - $breadcrumbs->push(trans('firefly.everything'), route('tags.show', [$tag->id], $moment)); + $breadcrumbs->push(trans('firefly.everything'), route('tags.show', [$tag->id, $moment])); } - if ($moment !== 'all') { + // when is specific period or when empty: + if ($moment !== 'all' && $moment !== '(nothing)') { $title = trans( 'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))), 'end' => $end->formatLocalized(strval(trans('config.month_and_day')))] ); - $breadcrumbs->push($title, route('tags.show', [$tag->id], $moment)); + $breadcrumbs->push($title, route('tags.show', [$tag->id, $moment])); } } ); @@ -753,8 +748,8 @@ Breadcrumbs::register( $breadcrumbs->push(trans('firefly.everything'), route('transactions.index', [$what, 'all'])); } - // when is specific period: - if ($moment !== 'all') { + // when is specific period or when empty: + if ($moment !== 'all' && $moment !== '(nothing)') { $title = trans( 'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))), 'end' => $end->formatLocalized(strval(trans('config.month_and_day')))] @@ -767,7 +762,7 @@ Breadcrumbs::register( Breadcrumbs::register( 'transactions.create', function (BreadCrumbGenerator $breadcrumbs, string $what) { - $breadcrumbs->parent('transactions.index', $what, '', new Carbon, new Carbon); + $breadcrumbs->parent('transactions.index', $what, '(nothing)', new Carbon, new Carbon); $breadcrumbs->push(trans('breadcrumbs.create_' . e($what)), route('transactions.create', [$what])); } ); @@ -789,7 +784,7 @@ Breadcrumbs::register( 'transactions.show', function (BreadCrumbGenerator $breadcrumbs, TransactionJournal $journal) { $what = strtolower($journal->transactionType->type); - $breadcrumbs->parent('transactions.index', $what, '', new Carbon, new Carbon); + $breadcrumbs->parent('transactions.index', $what, '(nothing)', new Carbon, new Carbon); $breadcrumbs->push($journal->description, route('transactions.show', [$journal->id])); } ); @@ -814,7 +809,7 @@ Breadcrumbs::register( if ($journals->count() > 0) { $journalIds = $journals->pluck('id')->toArray(); $what = strtolower($journals->first()->transactionType->type); - $breadcrumbs->parent('transactions.index', $what, '', new Carbon, new Carbon); + $breadcrumbs->parent('transactions.index', $what, '(nothing)', new Carbon, new Carbon); $breadcrumbs->push(trans('firefly.mass_edit_journals'), route('transactions.mass.edit', $journalIds)); return; @@ -829,7 +824,7 @@ Breadcrumbs::register( $journalIds = $journals->pluck('id')->toArray(); $what = strtolower($journals->first()->transactionType->type); - $breadcrumbs->parent('transactions.index', $what, '', new Carbon, new Carbon); + $breadcrumbs->parent('transactions.index', $what, '(nothing)', new Carbon, new Carbon); $breadcrumbs->push(trans('firefly.mass_edit_journals'), route('transactions.mass.delete', $journalIds)); } ); diff --git a/app/Import/Configurator/ConfiguratorInterface.php b/app/Import/Configurator/ConfiguratorInterface.php new file mode 100644 index 0000000000..1f120d30d3 --- /dev/null +++ b/app/Import/Configurator/ConfiguratorInterface.php @@ -0,0 +1,65 @@ +getConfigurationClass(); + $job = $this->job; + /** @var ConfigurationInterface $object */ + $object = new $class($this->job); + $object->setJob($job); + + return $object->storeConfiguration($data); + } + + /** + * Return the data required for the next step in the job configuration. + * + * @return array + * @throws FireflyException + */ + public function getNextData(): array + { + $class = $this->getConfigurationClass(); + $job = $this->job; + /** @var ConfigurationInterface $object */ + $object = app($class); + $object->setJob($job); + + return $object->getData(); + + } + + /** + * @return string + * @throws FireflyException + */ + public function getNextView(): string + { + if (!$this->job->configuration['initial-config-complete']) { + return 'import.csv.initial'; + } + if (!$this->job->configuration['column-roles-complete']) { + return 'import.csv.roles'; + } + if (!$this->job->configuration['column-mapping-complete']) { + return 'import.csv.map'; + } + + throw new FireflyException('No view for state'); + } + + /** + * @return bool + */ + public function isJobConfigured(): bool + { + $config = $this->job->configuration; + $config['initial-config-complete'] = $config['initial-config-complete'] ?? false; + $config['column-roles-complete'] = $config['column-roles-complete'] ?? false; + $config['column-mapping-complete'] = $config['column-mapping-complete'] ?? false; + $this->job->configuration = $config; + $this->job->save(); + + if ($this->job->configuration['initial-config-complete'] + && $this->job->configuration['column-roles-complete'] + && $this->job->configuration['column-mapping-complete'] + ) { + return true; + } + + return false; + } + + /** + * @param ImportJob $job + */ + public function setJob(ImportJob $job) + { + $this->job = $job; + if (is_null($this->job->configuration) || count($this->job->configuration) === 0) { + Log::debug(sprintf('Gave import job %s initial configuration.', $this->job->key)); + $this->job->configuration = config('csv.default_config'); + $this->job->save(); + } + } + + /** + * @return string + * @throws FireflyException + */ + private function getConfigurationClass(): string + { + $class = false; + switch (true) { + case (!$this->job->configuration['initial-config-complete']): + $class = Initial::class; + break; + case (!$this->job->configuration['column-roles-complete']): + $class = Roles::class; + break; + case (!$this->job->configuration['column-mapping-complete']): + $class = Map::class; + break; + default: + break; + } + + if ($class === false || strlen($class) === 0) { + throw new FireflyException('Cannot handle current job state in getConfigurationClass().'); + } + if (!class_exists($class)) { + throw new FireflyException(sprintf('Class %s does not exist in getConfigurationClass().', $class)); + } + + return $class; + } +} diff --git a/app/Import/Converter/AccountId.php b/app/Import/Converter/AccountId.php deleted file mode 100644 index 88161515ab..0000000000 --- a/app/Import/Converter/AccountId.php +++ /dev/null @@ -1,69 +0,0 @@ - $value]); - if ($value === 0) { - $this->setCertainty(0); - - return new Account; - } - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - - if (isset($this->mapping[$value])) { - Log::debug('Found account in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $account = $repository->find(intval($this->mapping[$value])); - if (!is_null($account->id)) { - Log::debug('Found account by ID', ['id' => $account->id]); - - $this->setCertainty(100); - - return $account; - } - } - $account = $repository->find($value);// not mapped? Still try to find it first: - if (!is_null($account->id)) { - $this->setCertainty(90); - Log::debug('Found account by ID ', ['id' => $account->id]); - - return $account; - } - $this->setCertainty(0); // should not really happen. If the ID does not match FF, what is FF supposed to do? - - return new Account; - - } -} diff --git a/app/Import/Converter/Amount.php b/app/Import/Converter/Amount.php index c7840e397c..75f90a534d 100644 --- a/app/Import/Converter/Amount.php +++ b/app/Import/Converter/Amount.php @@ -18,7 +18,7 @@ namespace FireflyIII\Import\Converter; * * @package FireflyIII\Import\Converter */ -class Amount extends BasicConverter implements ConverterInterface +class Amount implements ConverterInterface { /** @@ -28,18 +28,18 @@ class Amount extends BasicConverter implements ConverterInterface * * @param $value * - * @return float + * @return string */ - public function convert($value): float + public function convert($value): string { $len = strlen($value); $decimalPosition = $len - 3; $decimal = null; - if (($len > 2 && $value{$decimalPosition} == '.') || ($len > 2 && strpos($value, '.') > $decimalPosition)) { + if (($len > 2 && $value{$decimalPosition} === '.') || ($len > 2 && strpos($value, '.') > $decimalPosition)) { $decimal = '.'; } - if ($len > 2 && $value{$decimalPosition} == ',') { + if ($len > 2 && $value{$decimalPosition} === ',') { $decimal = ','; } @@ -59,10 +59,7 @@ class Amount extends BasicConverter implements ConverterInterface $value = str_replace($search, '', $value); } - $this->setCertainty(90); - - - return round(floatval($value), 12); + return strval(round(floatval($value), 12)); } } diff --git a/app/Import/Converter/AssetAccountIban.php b/app/Import/Converter/AssetAccountIban.php deleted file mode 100644 index f98bef6110..0000000000 --- a/app/Import/Converter/AssetAccountIban.php +++ /dev/null @@ -1,87 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new Account; - } - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - - - if (isset($this->mapping[$value])) { - Log::debug('Found account in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $account = $repository->find(intval($this->mapping[$value])); - if (!is_null($account->id)) { - $this->setCertainty(100); - Log::debug('Found account by ID', ['id' => $account->id]); - - return $account; - } - } - - // not mapped? Still try to find it first: - $account = $repository->findByIban($value, [AccountType::ASSET]); - if (!is_null($account->id)) { - Log::debug('Found account by IBAN', ['id' => $account->id]); - $this->setCertainty(50); - - return $account; - } - - - $account = $repository->store( - ['name' => 'Asset account with IBAN ' . $value, 'iban' => $value, 'user' => $this->user->id, 'accountType' => 'asset', 'virtualBalance' => 0, - 'active' => true, 'openingBalance' => 0] - ); - - if (is_null($account->id)) { - $this->setCertainty(0); - Log::info('Could not store new asset account by IBAN', $account->getErrors()->toArray()); - - return new Account; - } - - $this->setCertainty(100); - - return $account; - } -} diff --git a/app/Import/Converter/AssetAccountName.php b/app/Import/Converter/AssetAccountName.php deleted file mode 100644 index 8355c3b056..0000000000 --- a/app/Import/Converter/AssetAccountName.php +++ /dev/null @@ -1,90 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new Account; - } - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - - - if (isset($this->mapping[$value])) { - Log::debug('Found account in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $account = $repository->find(intval($this->mapping[$value])); - if (!is_null($account->id)) { - Log::debug('Found account by ID', ['id' => $account->id]); - $this->setCertainty(100); - - return $account; - } - } - - // not mapped? Still try to find it first: - $account = $repository->findByName($value, [AccountType::ASSET]); - if (!is_null($account->id)) { - Log::debug('Found asset account by name', ['value' => $value, 'id' => $account->id]); - - return $account; - } - - - $account = $repository->store( - ['name' => $value, 'iban' => null, 'openingBalance' => 0, 'user' => $this->user->id, 'accountType' => 'asset', 'virtualBalance' => 0, - 'active' => true] - ); - - if (is_null($account->id)) { - $this->setCertainty(0); - Log::info('Could not store new asset account by name', $account->getErrors()->toArray()); - - return new Account; - } - - $this->setCertainty(100); - - Log::debug('Created new asset account ', ['name' => $account->name, 'id' => $account->id]); - - return $account; - - - } -} diff --git a/app/Import/Converter/AssetAccountNumber.php b/app/Import/Converter/AssetAccountNumber.php deleted file mode 100644 index 880b4d3061..0000000000 --- a/app/Import/Converter/AssetAccountNumber.php +++ /dev/null @@ -1,96 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - return new Account; - } - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - - - if (isset($this->mapping[$value])) { - Log::debug('Found account in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $account = $repository->find(intval($this->mapping[$value])); - if (!is_null($account->id)) { - Log::debug('Found account by ID', ['id' => $account->id]); - - return $account; - } - } - - // not mapped? Still try to find it first: - $account = $repository->findByAccountNumber($value, [AccountType::ASSET]); - if (!is_null($account->id)) { - Log::debug('Found account by name', ['id' => $account->id]); - $this->setCertainty(50); - - return $account; - } - - // try to find by the name we would give it: - $accountName = 'Asset account with number ' . e($value); - $account = $repository->findByName($accountName, [AccountType::ASSET]); - if (!is_null($account->id)) { - Log::debug('Found account by name', ['id' => $account->id]); - $this->setCertainty(50); - - return $account; - } - - - $account = $repository->store( - ['name' => $accountName, 'openingBalance' => 0, 'iban' => null, 'user' => $this->user->id, - 'accountType' => 'asset', - 'virtualBalance' => 0, 'accountNumber' => $value, 'active' => true] - ); - - if (is_null($account->id)) { - $this->setCertainty(0); - Log::info('Could not store new asset account by account number', $account->getErrors()->toArray()); - - return new Account; - } - - $this->setCertainty(100); - - return $account; - - } -} diff --git a/app/Import/Converter/BasicConverter.php b/app/Import/Converter/BasicConverter.php deleted file mode 100644 index 49fba30f0d..0000000000 --- a/app/Import/Converter/BasicConverter.php +++ /dev/null @@ -1,85 +0,0 @@ -certainty; - } - - /** - * @param int $certainty - */ - protected function setCertainty(int $certainty) - { - $this->certainty = $certainty; - } - - /** - * @param array $config - */ - public function setConfig(array $config) - { - $this->config = $config; - } - - /** - * @param mixed $doMap - */ - public function setDoMap(bool $doMap) - { - $this->doMap = $doMap; - } - - /** - * @param array $mapping - * - */ - public function setMapping(array $mapping) - { - $this->mapping = $mapping; - } - - /** - * @param User $user - */ - public function setUser(User $user) - { - $this->user = $user; - } -} diff --git a/app/Import/Converter/BillId.php b/app/Import/Converter/BillId.php deleted file mode 100644 index 11c91d6536..0000000000 --- a/app/Import/Converter/BillId.php +++ /dev/null @@ -1,76 +0,0 @@ - $value]); - - if ($value === 0) { - $this->setCertainty(0); - - return new Bill; - } - - /** @var BillRepositoryInterface $repository */ - $repository = app(BillRepositoryInterface::class); - $repository->setUser($this->user); - - if (isset($this->mapping[$value])) { - Log::debug('Found bill in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $bill = $repository->find(intval($this->mapping[$value])); - if (!is_null($bill->id)) { - Log::debug('Found bill by ID', ['id' => $bill->id]); - $this->setCertainty(100); - - return $bill; - } - } - - // not mapped? Still try to find it first: - $bill = $repository->find($value); - if (!is_null($bill->id)) { - Log::debug('Found bill by ID ', ['id' => $bill->id]); - $this->setCertainty(100); - - return $bill; - } - - // should not really happen. If the ID does not match FF, what is FF supposed to do? - Log::info(sprintf('Could not find bill with ID %d. Will return NULL', $value)); - - $this->setCertainty(0); - - return new Bill; - - } -} diff --git a/app/Import/Converter/BillName.php b/app/Import/Converter/BillName.php deleted file mode 100644 index 2ba4ad2761..0000000000 --- a/app/Import/Converter/BillName.php +++ /dev/null @@ -1,99 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new Bill; - } - - /** @var BillRepositoryInterface $repository */ - $repository = app(BillRepositoryInterface::class); - $repository->setUser($this->user); - - if (isset($this->mapping[$value])) { - Log::debug('Found bill in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $bill = $repository->find(intval($this->mapping[$value])); - if (!is_null($bill->id)) { - Log::debug('Found bill by ID', ['id' => $bill->id]); - $this->setCertainty(100); - - return $bill; - } - } - - // not mapped? Still try to find it first: - $bill = $repository->findByName($value); - if (!is_null($bill->id)) { - Log::debug('Found bill by name ', ['id' => $bill->id]); - $this->setCertainty(100); - - return $bill; - } - - // create new bill. Use a lot of made up values. - $bill = $repository->store( - [ - 'name' => $value, - 'match' => $value, - 'amount_min' => 1, - 'user' => $this->user->id, - 'amount_max' => 10, - 'date' => date('Ymd'), - 'repeat_freq' => 'monthly', - 'skip' => 0, - 'automatch' => 0, - 'active' => 1, - - ] - ); - if (is_null($bill->id)) { - $this->setCertainty(0); - Log::info('Could not store new bill by name', $bill->getErrors()->toArray()); - - return new Bill; - } - - $this->setCertainty(100); - - return $bill; - - - } -} diff --git a/app/Import/Converter/BudgetId.php b/app/Import/Converter/BudgetId.php deleted file mode 100644 index ea74b8302f..0000000000 --- a/app/Import/Converter/BudgetId.php +++ /dev/null @@ -1,76 +0,0 @@ - $value]); - - if ($value === 0) { - $this->setCertainty(0); - - return new Budget; - } - - /** @var BudgetRepositoryInterface $repository */ - $repository = app(BudgetRepositoryInterface::class); - $repository->setUser($this->user); - - if (isset($this->mapping[$value])) { - Log::debug('Found budget in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $budget = $repository->find(intval($this->mapping[$value])); - if (!is_null($budget->id)) { - Log::debug('Found budget by ID', ['id' => $budget->id]); - $this->setCertainty(100); - - return $budget; - } - } - - // not mapped? Still try to find it first: - $budget = $repository->find($value); - if (!is_null($budget->id)) { - Log::debug('Found budget by ID ', ['id' => $budget->id]); - $this->setCertainty(100); - - return $budget; - } - - // should not really happen. If the ID does not match FF, what is FF supposed to do? - $this->setCertainty(0); - - Log::info(sprintf('Could not find budget with ID %d. Will return NULL', $value)); - - return new Budget; - - } -} diff --git a/app/Import/Converter/BudgetName.php b/app/Import/Converter/BudgetName.php deleted file mode 100644 index 5d36a109ac..0000000000 --- a/app/Import/Converter/BudgetName.php +++ /dev/null @@ -1,80 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new Budget; - } - - /** @var BudgetRepositoryInterface $repository */ - $repository = app(BudgetRepositoryInterface::class); - $repository->setUser($this->user); - - if (isset($this->mapping[$value])) { - Log::debug('Found budget in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $budget = $repository->find(intval($this->mapping[$value])); - if (!is_null($budget->id)) { - Log::debug('Found budget by ID', ['id' => $budget->id]); - $this->setCertainty(100); - - return $budget; - } - } - - // not mapped? Still try to find it first: - $budget = $repository->findByName($value); - if (!is_null($budget->id)) { - Log::debug('Found budget by name ', ['id' => $budget->id]); - $this->setCertainty(100); - - return $budget; - } - - // create new budget. Use a lot of made up values. - $budget = $repository->store( - [ - 'name' => $value, - 'user' => $this->user->id, - ] - ); - $this->setCertainty(100); - - return $budget; - - } -} diff --git a/app/Import/Converter/CategoryId.php b/app/Import/Converter/CategoryId.php deleted file mode 100644 index 4b5cd4e6af..0000000000 --- a/app/Import/Converter/CategoryId.php +++ /dev/null @@ -1,76 +0,0 @@ - $value]); - - if ($value === 0) { - $this->setCertainty(0); - - return new Category; - } - - /** @var CategoryRepositoryInterface $repository */ - $repository = app(CategoryRepositoryInterface::class); - $repository->setUser($this->user); - - if (isset($this->mapping[$value])) { - Log::debug('Found category in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $category = $repository->find(intval($this->mapping[$value])); - if (!is_null($category->id)) { - Log::debug('Found category by ID', ['id' => $category->id]); - $this->setCertainty(100); - - return $category; - } - } - - // not mapped? Still try to find it first: - $category = $repository->find($value); - if (!is_null($category->id)) { - Log::debug('Found category by ID ', ['id' => $category->id]); - $this->setCertainty(100); - - return $category; - } - - // should not really happen. If the ID does not match FF, what is FF supposed to do? - $this->setCertainty(0); - - Log::info(sprintf('Could not find category with ID %d. Will return NULL', $value)); - - return new Category; - - } -} diff --git a/app/Import/Converter/CategoryName.php b/app/Import/Converter/CategoryName.php deleted file mode 100644 index fcd52413cc..0000000000 --- a/app/Import/Converter/CategoryName.php +++ /dev/null @@ -1,80 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new Category; - } - - /** @var CategoryRepositoryInterface $repository */ - $repository = app(CategoryRepositoryInterface::class); - $repository->setUser($this->user); - - if (isset($this->mapping[$value])) { - Log::debug('Found category in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $category = $repository->find(intval($this->mapping[$value])); - if (!is_null($category->id)) { - Log::debug('Found category by ID', ['id' => $category->id]); - $this->setCertainty(100); - - return $category; - } - } - - // not mapped? Still try to find it first: - $category = $repository->findByName($value); - if (!is_null($category->id)) { - Log::debug('Found category by name ', ['id' => $category->id]); - $this->setCertainty(100); - - return $category; - } - - // create new category. Use a lot of made up values. - $category = $repository->store( - [ - 'name' => $value, - 'user' => $this->user->id, - ] - ); - $this->setCertainty(100); - - return $category; - - } -} diff --git a/app/Import/Converter/ConverterInterface.php b/app/Import/Converter/ConverterInterface.php index f5c27a2746..010d06e4ca 100644 --- a/app/Import/Converter/ConverterInterface.php +++ b/app/Import/Converter/ConverterInterface.php @@ -13,8 +13,6 @@ declare(strict_types=1); namespace FireflyIII\Import\Converter; -use FireflyIII\User; - /** * Interface ConverterInterface * @@ -27,30 +25,4 @@ interface ConverterInterface * */ public function convert($value); - - /** - * @return int - */ - public function getCertainty(): int; - - /** - * @param array $config - */ - public function setConfig(array $config); - - /** - * @param bool $doMap - */ - public function setDoMap(bool $doMap); - - /** - * @param array $mapping - * - */ - public function setMapping(array $mapping); - - /** - * @param User $user - */ - public function setUser(User $user); } diff --git a/app/Import/Converter/CurrencyCode.php b/app/Import/Converter/CurrencyCode.php deleted file mode 100644 index 1afa778292..0000000000 --- a/app/Import/Converter/CurrencyCode.php +++ /dev/null @@ -1,71 +0,0 @@ - $value]); - - /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class); - $repository->setUser($this->user); - - if (isset($this->mapping[$value])) { - Log::debug('Found currency in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $currency = $repository->find(intval($this->mapping[$value])); - if (!is_null($currency->id)) { - Log::debug('Found currency by ID', ['id' => $currency->id]); - $this->setCertainty(100); - - return $currency; - } - } - - // not mapped? Still try to find it first: - $currency = $repository->findByCode($value); - if (!is_null($currency->id)) { - Log::debug('Found currency by code', ['id' => $currency->id]); - $this->setCertainty(100); - - return $currency; - } - $currency = $repository->store( - [ - 'name' => $value, - 'code' => $value, - 'symbol' => $value, - ] - ); - $this->setCertainty(100); - - return $currency; - } -} diff --git a/app/Import/Converter/CurrencyId.php b/app/Import/Converter/CurrencyId.php deleted file mode 100644 index d3b74da000..0000000000 --- a/app/Import/Converter/CurrencyId.php +++ /dev/null @@ -1,75 +0,0 @@ - $value]); - - if ($value === 0) { - $this->setCertainty(0); - - return new TransactionCurrency; - } - - /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class); - $repository->setUser($this->user); - - if (isset($this->mapping[$value])) { - Log::debug('Found currency in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $currency = $repository->find(intval($this->mapping[$value])); - if (!is_null($currency->id)) { - Log::debug('Found currency by ID', ['id' => $currency->id]); - $this->setCertainty(100); - - return $currency; - } - } - - // not mapped? Still try to find it first: - $currency = $repository->find($value); - if (!is_null($currency->id)) { - Log::debug('Found currency by ID ', ['id' => $currency->id]); - $this->setCertainty(100); - - return $currency; - } - $this->setCertainty(0); - // should not really happen. If the ID does not match FF, what is FF supposed to do? - - Log::info(sprintf('Could not find category with ID %d. Will return NULL', $value)); - - return new TransactionCurrency; - - } -} diff --git a/app/Import/Converter/CurrencyName.php b/app/Import/Converter/CurrencyName.php deleted file mode 100644 index f68ec043a1..0000000000 --- a/app/Import/Converter/CurrencyName.php +++ /dev/null @@ -1,81 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new TransactionCurrency; - } - - /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class); - $repository->setUser($this->user); - - if (isset($this->mapping[$value])) { - Log::debug('Found currency in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $currency = $repository->find(intval($this->mapping[$value])); - if (!is_null($currency->id)) { - Log::debug('Found currency by ID', ['id' => $currency->id]); - $this->setCertainty(100); - - return $currency; - } - } - - // not mapped? Still try to find it first: - $currency = $repository->findByName($value); - if (!is_null($currency->id)) { - Log::debug('Found currency by name ', ['id' => $currency->id]); - $this->setCertainty(100); - - return $currency; - } - - // create new currency - $currency = $repository->store( - [ - 'name' => $value, - 'code' => strtoupper(substr($value, 0, 3)), - 'symbol' => strtoupper(substr($value, 0, 1)), - ] - ); - $this->setCertainty(100); - - return $currency; - - } -} diff --git a/app/Import/Converter/CurrencySymbol.php b/app/Import/Converter/CurrencySymbol.php deleted file mode 100644 index a40b06af40..0000000000 --- a/app/Import/Converter/CurrencySymbol.php +++ /dev/null @@ -1,81 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new TransactionCurrency; - } - - /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class); - $repository->setUser($this->user); - - if (isset($this->mapping[$value])) { - Log::debug('Found currency in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $currency = $repository->find(intval($this->mapping[$value])); - if (!is_null($currency->id)) { - Log::debug('Found currency by ID', ['id' => $currency->id]); - $this->setCertainty(100); - - return $currency; - } - } - - // not mapped? Still try to find it first: - $currency = $repository->findBySymbol($value); - if (!is_null($currency->id)) { - Log::debug('Found currency by symbol ', ['id' => $currency->id]); - $this->setCertainty(100); - - return $currency; - } - - // create new currency - $currency = $repository->store( - [ - 'name' => 'Currency ' . $value, - 'code' => $value, - 'symbol' => $value, - ] - ); - $this->setCertainty(100); - - return $currency; - - } -} diff --git a/app/Import/Converter/Date.php b/app/Import/Converter/Date.php deleted file mode 100644 index b799aed9e7..0000000000 --- a/app/Import/Converter/Date.php +++ /dev/null @@ -1,53 +0,0 @@ - $value]); - Log::debug('Format: ', ['format' => $this->config['date-format']]); - try { - $date = Carbon::createFromFormat($this->config['date-format'], $value); - } catch (InvalidArgumentException $e) { - Log::info($e->getMessage()); - Log::info('Cannot convert this string using the given format.', ['value' => $value, 'format' => $this->config['date-format']]); - $this->setCertainty(0); - - return new Carbon; - } - Log::debug('Converted date', ['converted' => $date->toAtomString()]); - $this->setCertainty(100); - - return $date; - } -} diff --git a/app/Import/Converter/Description.php b/app/Import/Converter/Description.php deleted file mode 100644 index 9eb507acb8..0000000000 --- a/app/Import/Converter/Description.php +++ /dev/null @@ -1,39 +0,0 @@ -setCertainty(100); - - return strval($value); - - } -} diff --git a/app/Import/Converter/ExternalId.php b/app/Import/Converter/ExternalId.php deleted file mode 100644 index feb2e8c3d1..0000000000 --- a/app/Import/Converter/ExternalId.php +++ /dev/null @@ -1,39 +0,0 @@ -setCertainty(100); - - return strval(trim($value)); - - } -} diff --git a/app/Import/Converter/INGDebetCredit.php b/app/Import/Converter/INGDebetCredit.php index 80650257ae..7fa82f2cf6 100644 --- a/app/Import/Converter/INGDebetCredit.php +++ b/app/Import/Converter/INGDebetCredit.php @@ -20,7 +20,7 @@ use Log; * * @package FireflyIII\Import\Converter */ -class INGDebetCredit extends BasicConverter implements ConverterInterface +class INGDebetCredit implements ConverterInterface { /** @@ -34,12 +34,10 @@ class INGDebetCredit extends BasicConverter implements ConverterInterface if ($value === 'Af') { Log::debug('Return -1'); - $this->setCertainty(100); return -1; } - $this->setCertainty(100); Log::debug('Return 1'); return 1; diff --git a/app/Import/Converter/Ignore.php b/app/Import/Converter/Ignore.php deleted file mode 100644 index ac619bac3a..0000000000 --- a/app/Import/Converter/Ignore.php +++ /dev/null @@ -1,34 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new Account; - } - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - - - if (isset($this->mapping[$value])) { - Log::debug('Found account in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $account = $repository->find(intval($this->mapping[$value])); - if (!is_null($account->id)) { - Log::debug('Found account by ID', ['id' => $account->id]); - $this->setCertainty(100); - - return $account; - } - } - - // not mapped? Still try to find it first: - $account = $repository->findByIban($value, []); - if (!is_null($account->id)) { - Log::debug('Found account by IBAN', ['id' => $account->id]); - Log::info( - 'The match between IBAN and account is uncertain because the type of transactions may not have been determined.', - ['id' => $account->id, 'iban' => $value] - ); - $this->setCertainty(50); - - return $account; - } - - // the IBAN given may not be a valid IBAN. If not, we cannot store by - // iban and we have no opposing account. There should be some kind of fall back - // routine. - try { - $account = $repository->store( - ['name' => $value, 'iban' => $value, 'user' => $this->user->id, 'accountType' => 'import', 'virtualBalance' => 0, 'active' => true, - 'openingBalance' => 0] - ); - $this->setCertainty(100); - } catch (FireflyException $e) { - Log::error($e); - - $account = new Account; - } - - return $account; - } -} diff --git a/app/Import/Converter/OpposingAccountName.php b/app/Import/Converter/OpposingAccountName.php deleted file mode 100644 index 90a959408c..0000000000 --- a/app/Import/Converter/OpposingAccountName.php +++ /dev/null @@ -1,89 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new Account; - } - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - - - if (isset($this->mapping[$value])) { - Log::debug('Found account in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $account = $repository->find(intval($this->mapping[$value])); - if (!is_null($account->id)) { - Log::debug('Found account by ID', ['id' => $account->id]); - $this->setCertainty(100); - - return $account; - } - } - - // not mapped? Still try to find it first: - $account = $repository->findByName($value, []); - if (!is_null($account->id)) { - Log::debug('Found opposing account by name', ['id' => $account->id]); - Log::info( - 'The match between name and account is uncertain because the type of transactions may not have been determined.', - ['id' => $account->id, 'name' => $value] - ); - $this->setCertainty(50); - - return $account; - } - - $account = $repository->store( - ['name' => $value, 'iban' => null, 'user' => $this->user->id, 'accountType' => 'import', 'virtualBalance' => 0, 'active' => true, - 'openingBalance' => 0, - ] - ); - if (is_null($account->id)) { - $this->setCertainty(0); - - return new Account; - } - $this->setCertainty(100); - - Log::debug('Created new opposing account ', ['name' => $account->name, 'id' => $account->id]); - - return $account; - } -} diff --git a/app/Import/Converter/OpposingAccountNumber.php b/app/Import/Converter/OpposingAccountNumber.php deleted file mode 100644 index 8ede15ed85..0000000000 --- a/app/Import/Converter/OpposingAccountNumber.php +++ /dev/null @@ -1,91 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new Account; - } - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - - - if (isset($this->mapping[$value])) { - Log::debug('Found account in mapping. Should exist.', ['value' => $value, 'map' => $this->mapping[$value]]); - $account = $repository->find(intval($this->mapping[$value])); - if (!is_null($account->id)) { - Log::debug('Found account by ID', ['id' => $account->id]); - $this->setCertainty(100); - - return $account; - } - } - - // not mapped? Still try to find it first: - $account = $repository->findByAccountNumber($value, []); - if (!is_null($account->id)) { - Log::debug('Found account by number', ['id' => $account->id]); - $this->setCertainty(50); - - return $account; - } - - // try to find by the name we would give it: - $accountName = 'Import account with number ' . e($value); - $account = $repository->findByName($accountName, [AccountType::IMPORT]); - if (!is_null($account->id)) { - Log::debug('Found account by name', ['id' => $account->id]); - $this->setCertainty(50); - - return $account; - } - - - $account = $repository->store( - ['name' => $accountName, 'openingBalance' => 0, 'iban' => null, 'user' => $this->user->id, - 'accountType' => 'import', - 'virtualBalance' => 0, 'accountNumber' => $value, 'active' => true] - ); - $this->setCertainty(100); - - return $account; - - } -} diff --git a/app/Import/Converter/RabobankDebetCredit.php b/app/Import/Converter/RabobankDebetCredit.php index 9b3e89314d..cef1a55607 100644 --- a/app/Import/Converter/RabobankDebetCredit.php +++ b/app/Import/Converter/RabobankDebetCredit.php @@ -20,7 +20,7 @@ use Log; * * @package FireflyIII\Import\Converter */ -class RabobankDebetCredit extends BasicConverter implements ConverterInterface +class RabobankDebetCredit implements ConverterInterface { /** @@ -34,13 +34,11 @@ class RabobankDebetCredit extends BasicConverter implements ConverterInterface if ($value === 'D') { Log::debug('Return -1'); - $this->setCertainty(100); return -1; } Log::debug('Return 1'); - $this->setCertainty(100); return 1; } diff --git a/app/Import/Converter/TagSplit.php b/app/Import/Converter/TagSplit.php deleted file mode 100644 index f5ebd034af..0000000000 --- a/app/Import/Converter/TagSplit.php +++ /dev/null @@ -1,86 +0,0 @@ -setUser($user); - - - /** @var string $part */ - foreach ($parts as $part) { - if (isset($mapping[$part])) { - Log::debug('Found tag in mapping. Should exist.', ['value' => $part, 'map' => $mapping[$part]]); - $tag = $repository->find(intval($mapping[$part])); - if (!is_null($tag->id)) { - Log::debug('Found tag by ID', ['id' => $tag->id]); - - $set->push($tag); - continue; - } - } - // not mapped? Still try to find it first: - $tag = $repository->findByTag($part); - if (!is_null($tag->id)) { - Log::debug('Found tag by name ', ['id' => $tag->id]); - - $set->push($tag); - } - if (is_null($tag->id)) { - // create new tag - $tag = $repository->store( - [ - 'tag' => $part, - 'date' => null, - 'description' => $part, - 'latitude' => null, - 'longitude' => null, - 'zoomLevel' => null, - 'tagMode' => 'nothing', - ] - ); - Log::debug('Created new tag', ['name' => $part, 'id' => $tag->id]); - $set->push($tag); - } - } - - return $set; - } - -} diff --git a/app/Import/Converter/TagsComma.php b/app/Import/Converter/TagsComma.php deleted file mode 100644 index 93e13698f0..0000000000 --- a/app/Import/Converter/TagsComma.php +++ /dev/null @@ -1,48 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new Collection; - } - $parts = array_unique(explode(',', $value)); - $set = TagSplit::createSetFromSplits($this->user, $this->mapping, $parts); - $this->setCertainty(100); - - return $set; - } -} diff --git a/app/Import/Converter/TagsSpace.php b/app/Import/Converter/TagsSpace.php deleted file mode 100644 index ae9635a8b7..0000000000 --- a/app/Import/Converter/TagsSpace.php +++ /dev/null @@ -1,49 +0,0 @@ - $value]); - - if (strlen($value) === 0) { - $this->setCertainty(0); - - return new Collection; - } - $parts = array_unique(explode(' ', $value)); - $set = TagSplit::createSetFromSplits($this->user, $this->mapping, $parts); - $this->setCertainty(100); - - return $set; - - } -} diff --git a/app/Import/FileProcessor/CsvProcessor.php b/app/Import/FileProcessor/CsvProcessor.php new file mode 100644 index 0000000000..5be25f5ed1 --- /dev/null +++ b/app/Import/FileProcessor/CsvProcessor.php @@ -0,0 +1,235 @@ +objects = new Collection; + $this->validSpecifics = array_keys(config('csv.import_specifics')); + $this->validConverters = array_keys(config('csv.import_roles')); + } + + /** + * @return Collection + */ + public function getObjects(): Collection + { + return $this->objects; + } + + /** + * Does the actual job: + * + * @return bool + */ + public function run(): bool + { + Log::debug('Now in CsvProcessor run(). Job is now running...'); + + $entries = $this->getImportArray(); + $index = 0; + Log::notice('Building importable objects from CSV file.'); + foreach ($entries as $index => $row) { + // verify if not exists already: + if ($this->rowAlreadyImported($row)) { + $message = sprintf('Row #%d has already been imported.', $index); + $this->job->addError($index, $message); + $this->job->addStepsDone(5); // all steps. + Log::info($message); + continue; + } + $this->objects->push($this->importRow($index, $row)); + $this->job->addStepsDone(1); + } + // if job has no step count, set it now: + $extended = $this->job->extended_status; + if ($extended['steps'] === 0) { + $extended['steps'] = $index * 5; + $this->job->extended_status = $extended; + $this->job->save(); + } + + + return true; + } + + /** + * @param ImportJob $job + * + * @return FileProcessorInterface + */ + public function setJob(ImportJob $job): FileProcessorInterface + { + $this->job = $job; + + return $this; + } + + /** + * Add meta data to the individual value and verify that it can be handled in a later stage. + * + * @param int $index + * @param string $value + * + * @return array + * @throws FireflyException + */ + private function annotateValue(int $index, string $value) + { + $value = trim($value); + $config = $this->job->configuration; + $role = $config['column-roles'][$index] ?? '_ignore'; + $mapped = $config['column-mapping-config'][$index][$value] ?? null; + + // throw error when not a valid converter. + if (!in_array($role, $this->validConverters)) { + throw new FireflyException(sprintf('"%s" is not a valid role.', $role)); + } + + $entry = [ + 'role' => $role, + 'value' => $value, + 'mapped' => $mapped, + ]; + + return $entry; + } + + /** + * @return Iterator + */ + private function getImportArray(): Iterator + { + $content = $this->job->uploadFileContents(); + $config = $this->job->configuration; + $reader = Reader::createFromString($content); + $reader->setDelimiter($config['delimiter']); + $start = $config['has-headers'] ? 1 : 0; + $results = $reader->setOffset($start)->fetch(); + Log::debug(sprintf('Created a CSV reader starting at offset %d', $start)); + + return $results; + } + + /** + * Take a row, build import journal by annotating each value and storing it in the import journal. + * + * @param int $index + * @param array $row + * + * @return ImportJournal + */ + private function importRow(int $index, array $row): ImportJournal + { + Log::debug(sprintf('Now at row %d', $index)); + $row = $this->specifics($row); + $journal = new ImportJournal; + $journal->setUser($this->job->user); + $journal->setHash(hash('sha256', json_encode($row))); + + foreach ($row as $rowIndex => $value) { + $value = trim($value); + if (strlen($value) > 0) { + $annotated = $this->annotateValue($rowIndex, $value); + Log::debug('Annotated value', $annotated); + $journal->setValue($annotated); + } + } + Log::debug('ImportJournal complete, returning.'); + + return $journal; + } + + /** + * Checks if the row has not been imported before. + * + * @param array $array + * + * @return bool + */ + private function rowAlreadyImported(array $array): bool + { + $string = json_encode($array); + $hash = hash('sha256', json_encode($string)); + $json = json_encode($hash); + $entry = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') + ->where('data', $json) + ->where('name', 'importHash') + ->first(); + if (!is_null($entry)) { + return true; + } + + return false; + + } + + /** + * And this is the point where the specifix go to work. + * + * @param array $row + * + * @return array + * @throws FireflyException + */ + private function specifics(array $row): array + { + $config = $this->job->configuration; + // + foreach ($config['specifics'] as $name => $enabled) { + + if (!in_array($name, $this->validSpecifics)) { + throw new FireflyException(sprintf('"%s" is not a valid class name', $name)); + } + + /** @var SpecificInterface $specific */ + $specific = app('FireflyIII\Import\Specifics\\' . $name); + + // it returns the row, possibly modified: + $row = $specific->run($row); + } + + return $row; + + } +} diff --git a/app/Import/FileProcessor/FileProcessorInterface.php b/app/Import/FileProcessor/FileProcessorInterface.php new file mode 100644 index 0000000000..0813246750 --- /dev/null +++ b/app/Import/FileProcessor/FileProcessorInterface.php @@ -0,0 +1,42 @@ +validFields as $value) { - $this->fields[$value] = null; - $this->certain[$value] = 0; - } - $this->errors = new Collection; - - } - - /** - * @param string $role - * @param int $certainty - * @param $convertedValue - * - * @throws FireflyException - */ - public function importValue(string $role, int $certainty, $convertedValue) - { - switch ($role) { - default: - Log::error('Import entry cannot handle object.', ['role' => $role]); - throw new FireflyException('Import entry cannot handle object of type "' . $role . '".'); - case 'hash': - $this->hash = $convertedValue; - - return; - case 'amount': - /* - * Easy enough. - */ - $this->setFloat('amount', $convertedValue, $certainty); - $this->applyMultiplier('amount'); // if present. - - return; - case 'account-id': - case 'account-iban': - case 'account-name': - case 'account-number': - $this->setObject('asset-account', $convertedValue, $certainty); - break; - case 'opposing-number': - case 'opposing-iban': - case 'opposing-id': - case 'opposing-name': - $this->setObject('opposing-account', $convertedValue, $certainty); - break; - case 'bill-id': - case 'bill-name': - $this->setObject('bill', $convertedValue, $certainty); - break; - case 'budget-id': - case 'budget-name': - $this->setObject('budget', $convertedValue, $certainty); - break; - case 'category-id': - case 'category-name': - $this->setObject('category', $convertedValue, $certainty); - break; - case 'currency-code': - case 'currency-id': - case 'currency-name': - case 'currency-symbol': - $this->setObject('currency', $convertedValue, $certainty); - break; - case 'date-transaction': - $this->setDate('date-transaction', $convertedValue, $certainty); - break; - - case 'date-interest': - $this->setDate('date-interest', $convertedValue, $certainty); - break; - case 'date-book': - $this->setDate('date-book', $convertedValue, $certainty); - break; - case 'date-process': - $this->setDate('date-process', $convertedValue, $certainty); - break; - case 'sepa-ct-id': - case 'sepa-db': - case 'sepa-ct-op': - case 'description': - $this->setAppendableString('description', $convertedValue); - break; - case '_ignore': - break; - case 'ing-debet-credit': - case 'rabo-debet-credit': - $this->manipulateFloat('amount', 'multiply', $convertedValue); - $this->applyMultiplier('amount'); // if present. - break; - case 'tags-comma': - case 'tags-space': - $this->appendCollection('tags', $convertedValue); - break; - case 'external-id': - $this->externalID = $convertedValue; - break; - - } - } - - /** - * @param User $user - */ - public function setUser(User $user) - { - $this->user = $user; - } - - /** - * @param string $field - * @param Collection $convertedValue - */ - private function appendCollection(string $field, Collection $convertedValue) - { - if (is_null($this->fields[$field])) { - $this->fields[$field] = new Collection; - } - $this->fields[$field] = $this->fields[$field]->merge($convertedValue); - } - - /** - * @param string $field - */ - private function applyMultiplier(string $field) - { - if ($this->fields[$field] != 0 && $this->amountMultiplier != 0) { - $this->fields[$field] = $this->fields[$field] * $this->amountMultiplier; - } - } - - /** - * @param string $field - * @param string $action - * @param $convertedValue - * - * @throws FireflyException - */ - private function manipulateFloat(string $field, string $action, $convertedValue) - { - switch ($action) { - default: - Log::error('Cannot handle manipulateFloat', ['field' => $field, 'action' => $action]); - throw new FireflyException('Cannot manipulateFloat with action ' . $action); - case 'multiply': - $this->amountMultiplier = $convertedValue; - break; - } - } - - /** - * @param string $field - * @param string $value - */ - private function setAppendableString(string $field, string $value) - { - $value = trim($value); - $this->fields[$field] .= ' ' . $value; - } - - /** - * @param string $field - * @param Carbon $date - * @param int $certainty - */ - private function setDate(string $field, Carbon $date, int $certainty) - { - if ($certainty > $this->certain[$field] && !is_null($date)) { - Log::debug(sprintf('ImportEntry: %s is now %s with certainty %d', $field, $date->format('Y-m-d'), $certainty)); - $this->fields[$field] = $date; - $this->certain[$field] = $certainty; - - return; - } - Log::info(sprintf('Will not set %s based on certainty %d (current certainty is %d) or NULL id.', $field, $certainty, $this->certain[$field])); - - } - - /** - * @param string $field - * @param float $value - * @param int $certainty - */ - private function setFloat(string $field, float $value, int $certainty) - { - if ($certainty > $this->certain[$field]) { - Log::debug(sprintf('ImportEntry: %s is now %f with certainty %d', $field, $value, $certainty)); - $this->fields[$field] = $value; - $this->certain[$field] = $certainty; - - return; - } - Log::info(sprintf('Will not set %s based on certainty %d (current certainty is %d).', $field, $certainty, $this->certain[$field])); - } - - /** - * @param string $field - * @param $object - * @param int $certainty - */ - private function setObject(string $field, $object, int $certainty) - { - if ($certainty > $this->certain[$field] && !is_null($object->id)) { - Log::debug(sprintf('ImportEntry: %s ID is now %d with certainty %d', $field, $object->id, $certainty)); - $this->fields[$field] = $object; - $this->certain[$field] = $certainty; - - return; - } - Log::info(sprintf('Will not set %s based on certainty %d (current certainty is %d) or NULL id.', $field, $certainty, $this->certain[$field])); - - } -} diff --git a/app/Import/ImportProcedure.php b/app/Import/ImportProcedure.php deleted file mode 100644 index 9150010dae..0000000000 --- a/app/Import/ImportProcedure.php +++ /dev/null @@ -1,85 +0,0 @@ -status = 'import_running'; - $job->save(); - - // create Importer - $valid = array_keys(config('firefly.import_formats')); - $class = 'INVALID'; - if (in_array($job->file_type, $valid)) { - $class = config('firefly.import_formats.' . $job->file_type); - } - - /** @var ImporterInterface $importer */ - $importer = app($class); - $importer->setJob($job); - - // create import entries - $collection = $importer->createImportEntries(); - - // validate / clean collection: - $validator = new ImportValidator($collection); - $validator->setUser($job->user); - $validator->setJob($job); - if ($job->configuration['import-account'] != 0) { - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($job->user); - $validator->setDefaultImportAccount($repository->find($job->configuration['import-account'])); - } - - $cleaned = $validator->clean(); - - // then import collection: - $storage = new ImportStorage($job->user, $cleaned); - $storage->setJob($job); - - // and run store routine: - $result = $storage->store(); - - // grab import tag: - $status = $job->extended_status; - $status['importTag'] = $storage->importTag->id; - $job->extended_status = $status; - $job->status = 'import_complete'; - $job->save(); - - return $result; - } - -} diff --git a/app/Import/ImportProcedureInterface.php b/app/Import/ImportProcedureInterface.php deleted file mode 100644 index ca2c6ff5da..0000000000 --- a/app/Import/ImportProcedureInterface.php +++ /dev/null @@ -1,33 +0,0 @@ -entries = $entries; - $this->user = $user; - $this->rules = $this->getUserRules(); - - } - - /** - * @param ImportJob $job - */ - public function setJob(ImportJob $job) - { - $this->job = $job; - } - - /** - * @param User $user - */ - public function setUser(User $user) - { - $this->user = $user; - } - - /** - * @return Collection - */ - public function store(): Collection - { - // create a tag to join the transactions. - $this->importTag = $this->createImportTag(); - $collection = new Collection; - Log::notice(sprintf('Started storing %d entry(ies).', $this->entries->count())); - foreach ($this->entries as $index => $entry) { - Log::debug(sprintf('--- import store start for row %d ---', $index)); - - // store entry: - $journal = $this->storeSingle($index, $entry); - $this->job->addStepsDone(1); - - // apply rules: - $this->applyRules($journal); - $this->job->addStepsDone(1); - - $collection->put($index, $journal); - } - Log::notice(sprintf('Finished storing %d entry(ies).', $collection->count())); - - return $collection; - } - - /** - * @param string $hash - * - * @return TransactionJournal - */ - private function alreadyImported(string $hash): TransactionJournal - { - - $meta = TransactionJournalMeta - ::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') - ->where('journal_meta.name', 'originalImportHash') - ->where('transaction_journals.user_id', $this->user->id) - ->where('journal_meta.data', json_encode($hash))->first(['journal_meta.*']); - if (!is_null($meta)) { - /** @var TransactionJournal $journal */ - $journal = $meta->transactionjournal; - if (intval($journal->completed) === 1) { - return $journal; - } - } - - return new TransactionJournal; - } - - /** - * @param TransactionJournal $journal - * - * @return bool - */ - private function applyRules(TransactionJournal $journal): bool - { - if ($this->rules->count() > 0) { - - /** @var Rule $rule */ - foreach ($this->rules as $rule) { - Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id)); - $processor = Processor::make($rule); - $processor->handleTransactionJournal($journal); - - if ($rule->stop_processing) { - return true; - } - } - } - - return true; - } - - /** - * @return Tag - */ - private function createImportTag(): Tag - { - /** @var TagRepositoryInterface $repository */ - $repository = app(TagRepositoryInterface::class); - $repository->setUser($this->user); - $data = [ - 'tag' => trans('firefly.import_with_key', ['key' => $this->job->key]), - 'date' => new Carbon, - 'description' => null, - 'latitude' => null, - 'longitude' => null, - 'zoomLevel' => null, - 'tagMode' => 'nothing', - ]; - $tag = $repository->store($data); - - return $tag; - } - - /** - * @return Collection - */ - private function getUserRules(): Collection - { - $set = Rule::distinct() - ->where('rules.user_id', $this->user->id) - ->leftJoin('rule_groups', 'rule_groups.id', '=', 'rules.rule_group_id') - ->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id') - ->where('rule_groups.active', 1) - ->where('rule_triggers.trigger_type', 'user_action') - ->where('rule_triggers.trigger_value', 'store-journal') - ->where('rules.active', 1) - ->orderBy('rule_groups.order', 'ASC') - ->orderBy('rules.order', 'ASC') - ->get(['rules.*', 'rule_groups.order']); - Log::debug(sprintf('Found %d user rules.', $set->count())); - - return $set; - - } - - /** - * @param $entry - * - * @return array - * @throws FireflyException - */ - private function storeAccounts($entry): array - { - // then create transactions. Single ones, unfortunately. - switch ($entry->fields['transaction-type']->type) { - default: - throw new FireflyException('ImportStorage cannot handle ' . $entry->fields['transaction-type']->type); - case TransactionType::WITHDRAWAL: - $source = $entry->fields['asset-account']; - $destination = $entry->fields['opposing-account']; - // make amount positive, if it is not. - break; - case TransactionType::DEPOSIT: - $source = $entry->fields['opposing-account']; - $destination = $entry->fields['asset-account']; - break; - case TransactionType::TRANSFER: - // depends on amount: - if ($entry->fields['amount'] < 0) { - $source = $entry->fields['asset-account']; - $destination = $entry->fields['opposing-account']; - break; - } - $destination = $entry->fields['asset-account']; - $source = $entry->fields['opposing-account']; - break; - } - - return [ - 'source' => $source, - 'destination' => $destination, - ]; - } - - /** - * @param TransactionJournal $journal - * @param ImportEntry $entry - */ - private function storeBill(TransactionJournal $journal, ImportEntry $entry) - { - - if (!is_null($entry->fields['bill']) && !is_null($entry->fields['bill']->id)) { - $journal->bill()->associate($entry->fields['bill']); - Log::debug('Attached bill', ['id' => $entry->fields['bill']->id, 'name' => $entry->fields['bill']->name]); - $journal->save(); - } - } - - /** - * @param TransactionJournal $journal - * @param ImportEntry $entry - */ - private function storeBudget(TransactionJournal $journal, ImportEntry $entry) - { - if (!is_null($entry->fields['budget']) && !is_null($entry->fields['budget']->id)) { - $journal->budgets()->save($entry->fields['budget']); - Log::debug('Attached budget', ['id' => $entry->fields['budget']->id, 'name' => $entry->fields['budget']->name]); - $journal->save(); - } - - } - - /** - * @param TransactionJournal $journal - * @param ImportEntry $entry - */ - private function storeCategory(TransactionJournal $journal, ImportEntry $entry) - { - if (!is_null($entry->fields['category']) && !is_null($entry->fields['category']->id)) { - $journal->categories()->save($entry->fields['category']); - Log::debug('Attached category', ['id' => $entry->fields['category']->id, 'name' => $entry->fields['category']->name]); - $journal->save(); - } - } - - /** - * @param $entry - * - * @return TransactionJournal - */ - private function storeJournal($entry): TransactionJournal - { - - $billId = is_null($entry->fields['bill']) || intval($entry->fields['bill']->id) === 0 ? null : intval($entry->fields['bill']->id); - $journalData = [ - 'user_id' => $entry->user->id, - 'transaction_type_id' => $entry->fields['transaction-type']->id, - 'bill_id' => $billId, - 'transaction_currency_id' => $entry->fields['currency']->id, - 'description' => $entry->fields['description'], - 'date' => $entry->fields['date-transaction'], - 'interest_date' => $entry->fields['date-interest'], - 'book_date' => $entry->fields['date-book'], - 'process_date' => $entry->fields['date-process'], - 'completed' => 0, - ]; - /** @var TransactionJournal $journal */ - $journal = TransactionJournal::create($journalData); - - foreach ($journal->getErrors()->all() as $err) { - Log::error('Error when storing journal: ' . $err); - } - Log::debug('Created journal', ['id' => $journal->id]); - - // save hash as meta value: - $meta = new TransactionJournalMeta; - $meta->name = 'originalImportHash'; - $meta->data = $entry->hash; - $meta->transactionJournal()->associate($journal); - $meta->save(); - - return $journal; - } - - /** - * @param int $index - * @param ImportEntry $entry - * - * @return TransactionJournal - * @throws FireflyException - */ - private function storeSingle(int $index, ImportEntry $entry): TransactionJournal - { - if ($entry->valid === false) { - Log::warning(sprintf('Cannot import row %d, because the entry is not valid.', $index)); - $errors = join(', ', $entry->errors->all()); - $errorText = sprintf('Row #%d: ' . $errors, $index); - $extendedStatus = $this->job->extended_status; - $extendedStatus['errors'][] = $errorText; - $this->job->extended_status = $extendedStatus; - $this->job->save(); - - return new TransactionJournal; - } - $alreadyImported = $this->alreadyImported($entry->hash); - if (!is_null($alreadyImported->id)) { - Log::warning(sprintf('Cannot import row %d, because it has already been imported (journal #%d).', $index, $alreadyImported->id)); - $errorText = trans( - 'firefly.import_double', - ['row' => $index, 'link' => route('transactions.show', [$alreadyImported->id]), 'description' => $alreadyImported->description] - ); - $extendedStatus = $this->job->extended_status; - $extendedStatus['errors'][] = $errorText; - $this->job->extended_status = $extendedStatus; - $this->job->save(); - - return new TransactionJournal; - } - - Log::debug(sprintf('Going to store row %d', $index)); - - - $journal = $this->storeJournal($entry); - $amount = Steam::positive($entry->fields['amount']); - $accounts = $this->storeAccounts($entry); - - // create new transactions. This is something that needs a rewrite for multiple/split transactions. - $sourceData = [ - 'account_id' => $accounts['source']->id, - 'transaction_journal_id' => $journal->id, - 'description' => null, - 'amount' => bcmul($amount, '-1'), - ]; - - $destinationData = [ - 'account_id' => $accounts['destination']->id, - 'transaction_journal_id' => $journal->id, - 'description' => null, - 'amount' => $amount, - ]; - - $one = Transaction::create($sourceData); - $two = Transaction::create($destinationData); - $error = false; - if (is_null($one->id)) { - Log::error('Could not create transaction 1.', $one->getErrors()->all()); - $error = true; - } - - if (is_null($two->id)) { - Log::error('Could not create transaction 1.', $two->getErrors()->all()); - $error = true; - } - - // respond to error - if ($error === true) { - $errorText = sprintf('Cannot import row %d, because an error occured when storing data.', $index); - Log::error($errorText); - $extendedStatus = $this->job->extended_status; - $extendedStatus['errors'][] = $errorText; - $this->job->extended_status = $extendedStatus; - $this->job->save(); - - return new TransactionJournal; - } - - Log::debug('Created transaction 1', ['id' => $one->id, 'account' => $one->account_id, 'account_name' => $accounts['source']->name]); - Log::debug('Created transaction 2', ['id' => $two->id, 'account' => $two->account_id, 'account_name' => $accounts['destination']->name]); - - $journal->completed = 1; - $journal->save(); - - // attach import tag. - $journal->tags()->save($this->importTag); - - // now attach budget and so on. - $this->storeBudget($journal, $entry); - $this->storeCategory($journal, $entry); - $this->storeBill($journal, $entry); - - return $journal; - } -} diff --git a/app/Import/ImportValidator.php b/app/Import/ImportValidator.php deleted file mode 100644 index 3a3a373b2b..0000000000 --- a/app/Import/ImportValidator.php +++ /dev/null @@ -1,438 +0,0 @@ -entries = $entries; - } - - /** - * Clean collection by filling in all the blanks. - */ - public function clean(): Collection - { - Log::notice(sprintf('Started validating %d entry(ies).', $this->entries->count())); - $newCollection = new Collection; - /** @var ImportEntry $entry */ - foreach ($this->entries as $index => $entry) { - Log::debug(sprintf('--- import validator start for row %d ---', $index)); - /* - * X Adds the date (today) if no date is present. - * X Determins the types of accounts involved (asset, expense, revenue). - * X Determins the type of transaction (withdrawal, deposit, transfer). - * - Determins the currency of the transaction. - * X Adds a default description if there isn't one present. - */ - $entry = $this->checkAmount($entry); - $entry = $this->setDate($entry); - $entry = $this->setAssetAccount($entry); - $entry = $this->setOpposingAccount($entry); - $entry = $this->cleanDescription($entry); - $entry = $this->setTransactionType($entry); - $entry = $this->setTransactionCurrency($entry); - - $newCollection->put($index, $entry); - $this->job->addStepsDone(1); - } - Log::notice(sprintf('Finished validating %d entry(ies).', $newCollection->count())); - - return $newCollection; - } - - /** - * @param Account $defaultImportAccount - */ - public function setDefaultImportAccount(Account $defaultImportAccount) - { - $this->defaultImportAccount = $defaultImportAccount; - } - - /** - * @param ImportJob $job - */ - public function setJob(ImportJob $job) - { - $this->job = $job; - } - - /** - * @param User $user - */ - public function setUser(User $user) - { - $this->user = $user; - } - - /** - * @param ImportEntry $entry - * - * @return ImportEntry - */ - private function checkAmount(ImportEntry $entry): ImportEntry - { - if ($entry->fields['amount'] == 0) { - $entry->valid = false; - $entry->errors->push('Amount of transaction is zero, cannot handle.'); - Log::warning('Amount of transaction is zero, cannot handle.'); - - return $entry; - } - Log::debug('Amount is OK.'); - - return $entry; - } - - /** - * @param ImportEntry $entry - * - * @return ImportEntry - */ - private function cleanDescription(ImportEntry $entry): ImportEntry - { - - if (!isset($entry->fields['description'])) { - Log::debug('Set empty transaction description because field was not set.'); - $entry->fields['description'] = '(empty transaction description)'; - - return $entry; - } - if (is_null($entry->fields['description'])) { - Log::debug('Set empty transaction description because field was null.'); - $entry->fields['description'] = '(empty transaction description)'; - - return $entry; - } - $entry->fields['description'] = trim($entry->fields['description']); - - if (strlen($entry->fields['description']) == 0) { - Log::debug('Set empty transaction description because field was empty.'); - $entry->fields['description'] = '(empty transaction description)'; - - return $entry; - } - Log::debug('Transaction description is OK.', ['description' => $entry->fields['description']]); - - return $entry; - } - - /** - * @param Account $account - * @param string $type - * - * @return Account - */ - private function convertAccount(Account $account, string $type): Account - { - $accountType = $account->accountType->type; - if ($accountType === $type) { - Log::debug(sprintf('Account %s already of type %s', $account->name, $type)); - - return $account; - } - // find it first by new type: - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - - $result = $repository->findByName($account->name, [$type]); - if (is_null($result->id)) { - // can convert account: - Log::debug(sprintf('No account named %s of type %s, create new account.', $account->name, $type)); - $result = $repository->store( - [ - 'user' => $this->user->id, - 'accountType' => config('firefly.shortNamesByFullName.' . $type), - 'name' => $account->name, - 'virtualBalance' => 0, - 'active' => true, - 'iban' => null, - 'openingBalance' => 0, - ] - ); - } - Log::debug( - sprintf( - 'Using another account named %s (#%d) of type %s, will use that one instead of %s (#%d)', $account->name, $result->id, $type, $account->name, - $account->id - ) - ); - - return $result; - - - } - - /** - * @return Account - */ - private function fallbackExpenseAccount(): Account - { - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - - $name = 'Unknown expense account'; - $result = $repository->findByName($name, [AccountType::EXPENSE]); - if (is_null($result->id)) { - $result = $repository->store( - ['name' => $name, 'iban' => null, 'openingBalance' => 0, 'user' => $this->user->id, 'accountType' => 'expense', 'virtualBalance' => 0, - 'active' => true] - ); - } - - return $result; - } - - /** - * @return Account - */ - private function fallbackRevenueAccount(): Account - { - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - - $name = 'Unknown revenue account'; - $result = $repository->findByName($name, [AccountType::REVENUE]); - - - if (is_null($result->id)) { - $result = $repository->store( - ['name' => $name, 'iban' => null, 'openingBalance' => 0, 'user' => $this->user->id, 'accountType' => 'revenue', 'virtualBalance' => 0, - 'active' => true] - ); - } - - return $result; - } - - /** - * @param ImportEntry $entry - * - * @return ImportEntry - */ - private function setAssetAccount(ImportEntry $entry): ImportEntry - { - if (is_null($entry->fields['asset-account'])) { - if (!is_null($this->defaultImportAccount)) { - Log::debug('Set asset account from default asset account'); - $entry->fields['asset-account'] = $this->defaultImportAccount; - - return $entry; - } - // default import is null? should not happen. Entry cannot be imported. - // set error message and block. - $entry->valid = false; - Log::warning('Cannot import entry. Asset account is NULL and import account is also NULL.'); - - return $entry; - } - Log::debug('Asset account is OK.', ['id' => $entry->fields['asset-account']->id, 'name' => $entry->fields['asset-account']->name]); - - return $entry; - } - - - /** - * @param ImportEntry $entry - * - * @return ImportEntry - */ - private function setDate(ImportEntry $entry): ImportEntry - { - if (is_null($entry->fields['date-transaction']) || $entry->certain['date-transaction'] == 0) { - // empty date field? find alternative. - $alternatives = ['date-book', 'date-interest', 'date-process']; - foreach ($alternatives as $alternative) { - if (!is_null($entry->fields[$alternative])) { - Log::debug(sprintf('Copied date-transaction from %s.', $alternative)); - $entry->fields['date-transaction'] = clone $entry->fields[$alternative]; - - return $entry; - } - } - // date is still null at this point - Log::debug('Set date-transaction to today.'); - $entry->fields['date-transaction'] = new Carbon; - - return $entry; - } - - // confidence is zero? - - Log::debug('Date-transaction is OK'); - - return $entry; - } - - /** - * @param ImportEntry $entry - * - * @return ImportEntry - */ - private function setOpposingAccount(ImportEntry $entry): ImportEntry - { - // empty opposing account. Create one based on amount. - if (is_null($entry->fields['opposing-account'])) { - - if ($entry->fields['amount'] < 0) { - // create or find general opposing expense account. - Log::debug('Created fallback expense account'); - $entry->fields['opposing-account'] = $this->fallbackExpenseAccount(); - - return $entry; - } - Log::debug('Created fallback revenue account'); - $entry->fields['opposing-account'] = $this->fallbackRevenueAccount(); - - return $entry; - } - - // opposing is of type "import". Convert to correct type (by amount): - $type = $entry->fields['opposing-account']->accountType->type; - if ($type == AccountType::IMPORT && $entry->fields['amount'] < 0) { - $account = $this->convertAccount($entry->fields['opposing-account'], AccountType::EXPENSE); - $entry->fields['opposing-account'] = $account; - Log::debug('Converted import account to expense account'); - - return $entry; - } - if ($type == AccountType::IMPORT && $entry->fields['amount'] > 0) { - $account = $this->convertAccount($entry->fields['opposing-account'], AccountType::REVENUE); - $entry->fields['opposing-account'] = $account; - Log::debug('Converted import account to revenue account'); - - return $entry; - } - // amount < 0, but opposing is revenue - if ($type == AccountType::REVENUE && $entry->fields['amount'] < 0) { - $account = $this->convertAccount($entry->fields['opposing-account'], AccountType::EXPENSE); - $entry->fields['opposing-account'] = $account; - Log::debug('Converted revenue account to expense account'); - - return $entry; - } - - // amount > 0, but opposing is expense - if ($type == AccountType::EXPENSE && $entry->fields['amount'] > 0) { - $account = $this->convertAccount($entry->fields['opposing-account'], AccountType::REVENUE); - $entry->fields['opposing-account'] = $account; - Log::debug('Converted expense account to revenue account'); - - return $entry; - } - // account type is OK - Log::debug('Opposing account is OK.'); - - return $entry; - - } - - /** - * @param ImportEntry $entry - * - * @return ImportEntry - */ - private function setTransactionCurrency(ImportEntry $entry): ImportEntry - { - if (is_null($entry->fields['currency'])) { - /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class); - $repository->setUser($this->user); - // is the default currency for the user or the system - $defaultCode = Preferences::getForUser($this->user, 'currencyPreference', config('firefly.default_currency', 'EUR'))->data; - - $entry->fields['currency'] = $repository->findByCode($defaultCode); - Log::debug(sprintf('Set currency to %s', $defaultCode)); - - return $entry; - } - Log::debug(sprintf('Currency is OK: %s', $entry->fields['currency']->code)); - - return $entry; - } - - /** - * @param ImportEntry $entry - * - * @return ImportEntry - */ - private function setTransactionType(ImportEntry $entry): ImportEntry - { - Log::debug(sprintf('Opposing account is of type %s', $entry->fields['opposing-account']->accountType->type)); - $type = $entry->fields['opposing-account']->accountType->type; - switch ($type) { - case AccountType::EXPENSE: - $entry->fields['transaction-type'] = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); - Log::debug('Transaction type is now withdrawal.'); - - return $entry; - case AccountType::REVENUE: - $entry->fields['transaction-type'] = TransactionType::whereType(TransactionType::DEPOSIT)->first(); - Log::debug('Transaction type is now deposit.'); - - return $entry; - case AccountType::DEFAULT: - case AccountType::ASSET: - $entry->fields['transaction-type'] = TransactionType::whereType(TransactionType::TRANSFER)->first(); - Log::debug('Transaction type is now transfer.'); - - return $entry; - } - Log::warning(sprintf('Opposing account is of type %s, cannot handle this.', $type)); - $entry->valid = false; - $entry->errors->push(sprintf('Opposing account is of type %s, cannot handle this.', $type)); - - return $entry; - } - - -} diff --git a/app/Import/Importer/CsvImporter.php b/app/Import/Importer/CsvImporter.php deleted file mode 100644 index 99f8c62a97..0000000000 --- a/app/Import/Importer/CsvImporter.php +++ /dev/null @@ -1,175 +0,0 @@ -collection = new Collection; - $this->validSpecifics = array_keys(config('csv.import_specifics')); - } - - /** - * Run the actual import - * - * @return Collection - */ - public function createImportEntries(): Collection - { - $config = $this->job->configuration; - $content = $this->job->uploadFileContents(); - - // create CSV reader. - $reader = Reader::createFromString($content); - $reader->setDelimiter($config['delimiter']); - $start = $config['has-headers'] ? 1 : 0; - $results = $reader->fetch(); - - Log::notice('Building importable objects from CSV file.'); - - foreach ($results as $index => $row) { - if ($index >= $start) { - $line = $index + 1; - Log::debug('----- import entry build start --'); - Log::debug(sprintf('Now going to import row %d.', $index)); - $importEntry = $this->importSingleRow($index, $row); - $this->collection->put($line, $importEntry); - /** - * 1. Build import entry. - * 2. Validate import entry. - * 3. Store journal. - * 4. Run rules. - */ - $this->job->addTotalSteps(4); - $this->job->addStepsDone(1); - } - } - Log::debug(sprintf('Import collection contains %d entries', $this->collection->count())); - Log::notice(sprintf('Built %d importable object(s) from your CSV file.', $this->collection->count())); - - return $this->collection; - } - - /** - * @param ImportJob $job - */ - public function setJob(ImportJob $job) - { - $this->job = $job; - } - - /** - * @param int $index - * @param array $row - * - * @return ImportEntry - * @throws FireflyException - */ - private function importSingleRow(int $index, array $row): ImportEntry - { - // create import object. This is where each entry ends up. - $object = new ImportEntry; - - Log::debug(sprintf('Now at row %d', $index)); - - // set some vars: - $object->setUser($this->job->user); - $config = $this->job->configuration; - $json = json_encode($row); - - if ($json === false) { - throw new FireflyException(sprintf('Could not process row #%d. Are you sure the uploaded file is encoded as "UTF-8"?', $index)); - } - - // hash the row: - $hash = hash('sha256', $json); - $object->importValue('hash', 100, $hash); - - // and this is the point where the specifix go to work. - foreach ($config['specifics'] as $name => $enabled) { - - if (!in_array($name, $this->validSpecifics)) { - throw new FireflyException(sprintf('"%s" is not a valid class name', $name)); - } - - /** @var SpecificInterface $specific */ - $specific = app('FireflyIII\Import\Specifics\\' . $name); - - // it returns the row, possibly modified: - $row = $specific->run($row); - } - - foreach ($row as $rowIndex => $value) { - // find the role for this column: - $role = $config['column-roles'][$rowIndex] ?? '_ignore'; - $doMap = $config['column-do-mapping'][$rowIndex] ?? false; - $validConverters = array_keys(config('csv.import_roles')); - - // throw error when not a valid converter. - if (!in_array($role, $validConverters)) { - throw new FireflyException(sprintf('"%s" is not a valid role.', $role)); - } - $converterClass = config('csv.import_roles.' . $role . '.converter'); - $mapping = $config['column-mapping-config'][$rowIndex] ?? []; - $className = sprintf('FireflyIII\\Import\\Converter\\%s', $converterClass); - /** @var ConverterInterface $converter */ - $converter = app($className); - // set some useful values for the converter: - $converter->setMapping($mapping); - $converter->setDoMap($doMap); - $converter->setUser($this->job->user); - $converter->setConfig($config); - - // run the converter for this value: - $convertedValue = $converter->convert($value); - $certainty = $converter->getCertainty(); - - // log it. - Log::debug('Value ', ['index' => $rowIndex, 'value' => $value, 'role' => $role]); - - // store in import entry: - Log::debug('Going to import', ['role' => $role, 'value' => $value, 'certainty' => $certainty]); - $object->importValue($role, $certainty, $convertedValue); - } - - - return $object; - - } -} diff --git a/app/Import/Importer/ImporterInterface.php b/app/Import/Importer/ImporterInterface.php deleted file mode 100644 index 06c18bd793..0000000000 --- a/app/Import/Importer/ImporterInterface.php +++ /dev/null @@ -1,38 +0,0 @@ -command = $command; - $this->changeLevel(env('LOG_LEVEL', 'debug')); + + $this->changeLevel(env('APP_LOG_LEVEL', 'info')); } /** @@ -56,9 +57,10 @@ class CommandHandler extends AbstractProcessingHandler */ private function changeLevel(string $level) { - $level = strtoupper($level); - if (defined(sprintf('Logger::%s', $level))) { - $this->setLevel(constant(sprintf('Logger::%s', $level))); + $level = strtoupper($level); + $reference = sprintf('\Monolog\Logger::%s', $level); + if (defined($reference)) { + $this->setLevel(constant($reference)); } } } diff --git a/app/Import/Mapper/AssetAccountIbans.php b/app/Import/Mapper/AssetAccountIbans.php index e94f502cbb..9d53bf6d8c 100644 --- a/app/Import/Mapper/AssetAccountIbans.php +++ b/app/Import/Mapper/AssetAccountIbans.php @@ -42,7 +42,7 @@ class AssetAccountIbans implements MapperInterface if (strlen($iban) > 0) { $topList[$account->id] = $account->iban . ' (' . $account->name . ')'; } - if (strlen($iban) == 0) { + if (strlen($iban) === 0) { $list[$account->id] = $account->name; } } @@ -50,7 +50,7 @@ class AssetAccountIbans implements MapperInterface asort($list); $list = $topList + $list; - $list = [0 => trans('csv.do_not_map')] + $list; + $list = [0 => trans('csv.map_do_not_map')] + $list; return $list; diff --git a/app/Import/Mapper/AssetAccounts.php b/app/Import/Mapper/AssetAccounts.php index 8335bea7e4..2679334fe3 100644 --- a/app/Import/Mapper/AssetAccounts.php +++ b/app/Import/Mapper/AssetAccounts.php @@ -47,7 +47,7 @@ class AssetAccounts implements MapperInterface asort($list); - $list = [0 => trans('csv.do_not_map')] + $list; + $list = [0 => trans('csv.map_do_not_map')] + $list; return $list; diff --git a/app/Import/Mapper/Bills.php b/app/Import/Mapper/Bills.php index 57d60b3013..a12ee950ea 100644 --- a/app/Import/Mapper/Bills.php +++ b/app/Import/Mapper/Bills.php @@ -40,7 +40,7 @@ class Bills implements MapperInterface } asort($list); - $list = [0 => trans('csv.do_not_map')] + $list; + $list = [0 => trans('csv.map_do_not_map')] + $list; return $list; diff --git a/app/Import/Mapper/Budgets.php b/app/Import/Mapper/Budgets.php index 37276ddcc0..88ca1e0d5e 100644 --- a/app/Import/Mapper/Budgets.php +++ b/app/Import/Mapper/Budgets.php @@ -41,7 +41,7 @@ class Budgets implements MapperInterface } asort($list); - $list = [0 => trans('csv.do_not_map')] + $list; + $list = [0 => trans('csv.map_do_not_map')] + $list; return $list; diff --git a/app/Import/Mapper/Categories.php b/app/Import/Mapper/Categories.php index 5144a06a41..30c81dfada 100644 --- a/app/Import/Mapper/Categories.php +++ b/app/Import/Mapper/Categories.php @@ -41,7 +41,7 @@ class Categories implements MapperInterface } asort($list); - $list = [0 => trans('csv.do_not_map')] + $list; + $list = [0 => trans('csv.map_do_not_map')] + $list; return $list; diff --git a/app/Import/Mapper/OpposingAccountIbans.php b/app/Import/Mapper/OpposingAccountIbans.php index bcf3372e10..121395cac4 100644 --- a/app/Import/Mapper/OpposingAccountIbans.php +++ b/app/Import/Mapper/OpposingAccountIbans.php @@ -48,7 +48,7 @@ class OpposingAccountIbans implements MapperInterface if (strlen($iban) > 0) { $topList[$account->id] = $account->iban . ' (' . $account->name . ')'; } - if (strlen($iban) == 0) { + if (strlen($iban) === 0) { $list[$account->id] = $account->name; } } @@ -56,7 +56,7 @@ class OpposingAccountIbans implements MapperInterface asort($list); $list = $topList + $list; - $list = [0 => trans('csv.do_not_map')] + $list; + $list = [0 => trans('csv.map_do_not_map')] + $list; return $list; diff --git a/app/Import/Mapper/OpposingAccounts.php b/app/Import/Mapper/OpposingAccounts.php index fd2d86a5bb..bd95610504 100644 --- a/app/Import/Mapper/OpposingAccounts.php +++ b/app/Import/Mapper/OpposingAccounts.php @@ -53,7 +53,7 @@ class OpposingAccounts implements MapperInterface asort($list); - $list = [0 => trans('csv.do_not_map')] + $list; + $list = [0 => trans('csv.map_do_not_map')] + $list; return $list; } diff --git a/app/Import/Mapper/Tags.php b/app/Import/Mapper/Tags.php index e549fd90ef..c1b7f324e0 100644 --- a/app/Import/Mapper/Tags.php +++ b/app/Import/Mapper/Tags.php @@ -40,7 +40,7 @@ class Tags implements MapperInterface } asort($list); - $list = [0 => trans('csv.do_not_map')] + $list; + $list = [0 => trans('csv.map_do_not_map')] + $list; return $list; diff --git a/app/Import/Mapper/TransactionCurrencies.php b/app/Import/Mapper/TransactionCurrencies.php index d24f083b0f..286f275214 100644 --- a/app/Import/Mapper/TransactionCurrencies.php +++ b/app/Import/Mapper/TransactionCurrencies.php @@ -36,7 +36,7 @@ class TransactionCurrencies implements MapperInterface asort($list); - $list = [0 => trans('csv.do_not_map')] + $list; + $list = [0 => trans('csv.map_do_not_map')] + $list; return $list; diff --git a/app/Import/Object/ImportAccount.php b/app/Import/Object/ImportAccount.php new file mode 100644 index 0000000000..ca5dba85d5 --- /dev/null +++ b/app/Import/Object/ImportAccount.php @@ -0,0 +1,344 @@ +expectedType = AccountType::ASSET; + $this->account = new Account; + $this->repository = app(AccountRepositoryInterface::class); + Log::debug('Created ImportAccount.'); + } + + /** + * @return Account + */ + public function getAccount(): Account + { + if (is_null($this->account->id)) { + $this->store(); + } + + return $this->account; + } + + /** + * @return string + */ + public function getExpectedType(): string + { + return $this->expectedType; + } + + /** + * @param string $expectedType + */ + public function setExpectedType(string $expectedType) + { + $this->expectedType = $expectedType; + } + + /** + * @param array $accountIban + */ + public function setAccountIban(array $accountIban) + { + $this->accountIban = $accountIban; + } + + /** + * @param array $value + */ + public function setAccountId(array $value) + { + $this->accountId = $value; + } + + /** + * @param array $accountName + */ + public function setAccountName(array $accountName) + { + $this->accountName = $accountName; + } + + /** + * @param array $accountNumber + */ + public function setAccountNumber(array $accountNumber) + { + $this->accountNumber = $accountNumber; + } + + /** + * @param int $defaultAccountId + */ + public function setDefaultAccountId(int $defaultAccountId) + { + $this->defaultAccountId = $defaultAccountId; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + $this->repository->setUser($user); + } + + /** + * @return Account + */ + private function findExistingObject(): Account + { + Log::debug('In findExistingObject() for Account'); + // 0: determin account type: + /** @var AccountType $accountType */ + $accountType = AccountType::whereType($this->expectedType)->first(); + + // 1: find by ID, iban or name (and type) + if (count($this->accountId) === 3) { + Log::debug(sprintf('Finding account of type %d and ID %d', $accountType->id, $this->accountId['value'])); + /** @var Account $account */ + $account = $this->user->accounts()->where('account_type_id', $accountType->id)->where('id', $this->accountId['value'])->first(); + if (!is_null($account)) { + Log::debug(sprintf('Found unmapped %s account by ID (#%d): %s', $this->expectedType, $account->id, $account->name)); + + return $account; + } + Log::debug('Found nothing.'); + } + /** @var Collection $accounts */ + $accounts = $this->repository->getAccountsByType([$accountType->type]); + // Two: find by IBAN (and type): + if (count($this->accountIban) === 3) { + $iban = $this->accountIban['value']; + Log::debug(sprintf('Finding account of type %d and IBAN %s', $accountType->id, $iban)); + $filtered = $accounts->filter( + function (Account $account) use ($iban) { + if ($account->iban === $iban) { + Log::debug( + sprintf('Found unmapped %s account by IBAN (#%d): %s (%s)', $this->expectedType, $account->id, $account->name, $account->iban) + ); + + return $account; + } + + return null; + } + ); + if ($filtered->count() === 1) { + return $filtered->first(); + } + Log::debug('Found nothing.'); + } + + // Three: find by name (and type): + if (count($this->accountName) === 3) { + $name = $this->accountName['value']; + Log::debug(sprintf('Finding account of type %d and name %s', $accountType->id, $name)); + $filtered = $accounts->filter( + function (Account $account) use ($name) { + if ($account->name === $name) { + Log::debug(sprintf('Found unmapped %s account by name (#%d): %s', $this->expectedType, $account->id, $account->name)); + + return $account; + } + + return null; + } + ); + + if ($filtered->count() === 1) { + return $filtered->first(); + } + Log::debug('Found nothing.'); + } + + // 4: do not search by account number. + Log::debug('Found NO existing accounts.'); + + return new Account; + + } + + /** + * @return Account + */ + private function findMappedObject(): Account + { + Log::debug('In findMappedObject() for Account'); + $fields = ['accountId', 'accountIban', 'accountNumber', 'accountName']; + foreach ($fields as $field) { + $array = $this->$field; + Log::debug(sprintf('Find mapped account based on field "%s" with value', $field), $array); + // check if a pre-mapped object exists. + $mapped = $this->getMappedObject($array); + if (!is_null($mapped->id)) { + Log::debug(sprintf('Found account #%d!', $mapped->id)); + + return $mapped; + } + + } + Log::debug('Found no account on mapped data or no map present.'); + + return new Account; + } + + /** + * @param array $array + * + * @return Account + */ + private function getMappedObject(array $array): Account + { + Log::debug('In getMappedObject() for Account'); + if (count($array) === 0) { + Log::debug('Array is empty, nothing will come of this.'); + + return new Account; + } + + if (array_key_exists('mapped', $array) && is_null($array['mapped'])) { + Log::debug(sprintf('No map present for value "%s". Return NULL.', $array['value'])); + + return new Account; + } + + Log::debug('Finding a mapped account based on', $array); + + $search = intval($array['mapped']); + $account = $this->repository->find($search); + + if (is_null($account->id)) { + Log::error(sprintf('There is no account with id #%d. Invalid mapping will be ignored!', $search)); + + return new Account; + } + // must be of the same type + // except when mapped is an asset, then it's fair game. + // which only shows that user must map very carefully. + if ($account->accountType->type !== $this->expectedType && $account->accountType->type !== AccountType::ASSET) { + Log::error( + sprintf( + 'Mapped account #%d is of type "%s" but we expect a "%s"-account. Mapping will be ignored.', $account->id, $account->accountType->type, + $this->expectedType + ) + ); + + return new Account; + } + + Log::debug(sprintf('Found account! #%d ("%s"). Return it', $account->id, $account->name)); + + return $account; + } + + /** + * @return bool + */ + private function store(): bool + { + // 1: find mapped object: + $mapped = $this->findMappedObject(); + if (!is_null($mapped->id)) { + $this->account = $mapped; + + return true; + } + // 2: find existing by given values: + $found = $this->findExistingObject(); + if (!is_null($found->id)) { + $this->account = $found; + + return true; + } + + // 3: if found nothing, retry the search with an asset account: + Log::debug('Will try to find an asset account just in case.'); + $oldExpectedType = $this->expectedType; + $this->expectedType = AccountType::ASSET; + $found = $this->findExistingObject(); + if (!is_null($found->id)) { + Log::debug('Found asset account!'); + $this->account = $found; + + return true; + } + $this->expectedType = $oldExpectedType; + + // if search for an asset account, fall back to given "default account" (mandatory) + if ($this->expectedType === AccountType::ASSET) { + $this->account = $this->repository->find($this->defaultAccountId); + Log::debug(sprintf('Fall back to default account #%d "%s"', $this->account->id, $this->account->name)); + + return true; + } + + Log::debug(sprintf('Found no account of type %s so must create one ourselves.', $this->expectedType)); + + $data = [ + 'accountType' => config('firefly.shortNamesByFullName.' . $this->expectedType), + 'name' => $this->accountName['value'] ?? '(no name)', + 'iban' => $this->accountIban['value'] ?? null, + 'active' => true, + 'virtualBalance' => null, + ]; + + $this->account = $this->repository->store($data); + Log::debug(sprintf('Successfully stored new account #%d: %s', $this->account->id, $this->account->name)); + + return true; + } + + +} diff --git a/app/Import/Object/ImportBill.php b/app/Import/Object/ImportBill.php new file mode 100644 index 0000000000..b8bbb18161 --- /dev/null +++ b/app/Import/Object/ImportBill.php @@ -0,0 +1,235 @@ +bill = new Bill; + $this->repository = app(BillRepositoryInterface::class); + Log::debug('Created ImportBill.'); + } + + /** + * @return Bill + */ + public function getBill(): Bill + { + if (is_null($this->bill->id)) { + $this->store(); + } + + return $this->bill; + } + + /** + * @param array $id + */ + public function setId(array $id) + { + $this->id = $id; + } + + /** + * @param array $name + */ + public function setName(array $name) + { + $this->name = $name; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + $this->repository->setUser($user); + } + + /** + * @return Bill + */ + private function findExistingObject(): Bill + { + Log::debug('In findExistingObject() for Bill'); + // 1: find by ID, or name + + if (count($this->id) === 3) { + Log::debug(sprintf('Finding bill with ID #%d', $this->id['value'])); + /** @var Bill $bill */ + $bill = $this->repository->find(intval($this->id['value'])); + if (!is_null($bill->id)) { + Log::debug(sprintf('Found unmapped bill by ID (#%d): %s', $bill->id, $bill->name)); + + return $bill; + } + Log::debug('Found nothing.'); + } + // 2: find by name + if (count($this->name) === 3) { + /** @var Collection $bills */ + $bills = $this->repository->getBills(); + $name = $this->name['value']; + Log::debug(sprintf('Finding bill with name %s', $name)); + $filtered = $bills->filter( + function (Bill $bill) use ($name) { + if ($bill->name === $name) { + Log::debug(sprintf('Found unmapped bill by name (#%d): %s', $bill->id, $bill->name)); + + return $bill; + } + + return null; + } + ); + + if ($filtered->count() === 1) { + return $filtered->first(); + } + Log::debug('Found nothing.'); + } + + // 4: do not search by account number. + Log::debug('Found NO existing bills.'); + + return new Bill; + + } + + /** + * @return Bill + */ + private function findMappedObject(): Bill + { + Log::debug('In findMappedObject() for Bill'); + $fields = ['id', 'name']; + foreach ($fields as $field) { + $array = $this->$field; + Log::debug(sprintf('Find mapped bill based on field "%s" with value', $field), $array); + // check if a pre-mapped object exists. + $mapped = $this->getMappedObject($array); + if (!is_null($mapped->id)) { + Log::debug(sprintf('Found bill #%d!', $mapped->id)); + + return $mapped; + } + + } + Log::debug('Found no bill on mapped data or no map present.'); + + return new Bill; + } + + /** + * @param array $array + * + * @return Bill + */ + private function getMappedObject(array $array): Bill + { + Log::debug('In getMappedObject() for Bill'); + if (count($array) === 0) { + Log::debug('Array is empty, nothing will come of this.'); + + return new Bill; + } + + if (array_key_exists('mapped', $array) && is_null($array['mapped'])) { + Log::debug(sprintf('No map present for value "%s". Return NULL.', $array['value'])); + + return new Bill; + } + + Log::debug('Finding a mapped bill based on', $array); + + $search = intval($array['mapped']); + $bill = $this->repository->find($search); + + if (is_null($bill->id)) { + Log::error(sprintf('There is no bill with id #%d. Invalid mapping will be ignored!', $search)); + + return new Bill; + } + + + Log::debug(sprintf('Found bill! #%d ("%s"). Return it', $bill->id, $bill->name)); + + return $bill; + } + + /** + * @return bool + */ + private function store(): bool + { + // 1: find mapped object: + $mapped = $this->findMappedObject(); + if (!is_null($mapped->id)) { + $this->bill = $mapped; + + return true; + } + // 2: find existing by given values: + $found = $this->findExistingObject(); + if (!is_null($found->id)) { + $this->bill = $found; + + return true; + } + $name = $this->name['value'] ?? ''; + + if (strlen($name) === 0) { + return true; + } + + Log::debug('Found no bill so must create one ourselves.'); + + $data = [ + 'name' => $name, + ]; + + $this->bill = $this->repository->store($data); + Log::debug(sprintf('Successfully stored new bill #%d: %s', $this->bill->id, $this->bill->name)); + + return true; + } + +} diff --git a/app/Import/Object/ImportBudget.php b/app/Import/Object/ImportBudget.php new file mode 100644 index 0000000000..cbc1e05460 --- /dev/null +++ b/app/Import/Object/ImportBudget.php @@ -0,0 +1,235 @@ +budget = new Budget; + $this->repository = app(BudgetRepositoryInterface::class); + Log::debug('Created ImportBudget.'); + } + + /** + * @return Budget + */ + public function getBudget(): Budget + { + if (is_null($this->budget->id)) { + $this->store(); + } + + return $this->budget; + } + + /** + * @param array $id + */ + public function setId(array $id) + { + $this->id = $id; + } + + /** + * @param array $name + */ + public function setName(array $name) + { + $this->name = $name; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + $this->repository->setUser($user); + } + + /** + * @return Budget + */ + private function findExistingObject(): Budget + { + Log::debug('In findExistingObject() for Budget'); + // 1: find by ID, or name + + if (count($this->id) === 3) { + Log::debug(sprintf('Finding budget with ID #%d', $this->id['value'])); + /** @var Budget $budget */ + $budget = $this->repository->find(intval($this->id['value'])); + if (!is_null($budget->id)) { + Log::debug(sprintf('Found unmapped budget by ID (#%d): %s', $budget->id, $budget->name)); + + return $budget; + } + Log::debug('Found nothing.'); + } + // 2: find by name + if (count($this->name) === 3) { + /** @var Collection $budgets */ + $budgets = $this->repository->getBudgets(); + $name = $this->name['value']; + Log::debug(sprintf('Finding budget with name %s', $name)); + $filtered = $budgets->filter( + function (Budget $budget) use ($name) { + if ($budget->name === $name) { + Log::debug(sprintf('Found unmapped budget by name (#%d): %s', $budget->id, $budget->name)); + + return $budget; + } + + return null; + } + ); + + if ($filtered->count() === 1) { + return $filtered->first(); + } + Log::debug('Found nothing.'); + } + + // 4: do not search by account number. + Log::debug('Found NO existing budgets.'); + + return new Budget; + + } + + /** + * @return Budget + */ + private function findMappedObject(): Budget + { + Log::debug('In findMappedObject() for Budget'); + $fields = ['id', 'name']; + foreach ($fields as $field) { + $array = $this->$field; + Log::debug(sprintf('Find mapped budget based on field "%s" with value', $field), $array); + // check if a pre-mapped object exists. + $mapped = $this->getMappedObject($array); + if (!is_null($mapped->id)) { + Log::debug(sprintf('Found budget #%d!', $mapped->id)); + + return $mapped; + } + + } + Log::debug('Found no budget on mapped data or no map present.'); + + return new Budget; + } + + /** + * @param array $array + * + * @return Budget + */ + private function getMappedObject(array $array): Budget + { + Log::debug('In getMappedObject() for Budget'); + if (count($array) === 0) { + Log::debug('Array is empty, nothing will come of this.'); + + return new Budget; + } + + if (array_key_exists('mapped', $array) && is_null($array['mapped'])) { + Log::debug(sprintf('No map present for value "%s". Return NULL.', $array['value'])); + + return new Budget; + } + + Log::debug('Finding a mapped budget based on', $array); + + $search = intval($array['mapped']); + $budget = $this->repository->find($search); + + if (is_null($budget->id)) { + Log::error(sprintf('There is no budget with id #%d. Invalid mapping will be ignored!', $search)); + + return new Budget; + } + + Log::debug(sprintf('Found budget! #%d ("%s"). Return it', $budget->id, $budget->name)); + + return $budget; + } + + /** + * @return bool + */ + private function store(): bool + { + // 1: find mapped object: + $mapped = $this->findMappedObject(); + if (!is_null($mapped->id)) { + $this->budget = $mapped; + + return true; + } + // 2: find existing by given values: + $found = $this->findExistingObject(); + if (!is_null($found->id)) { + $this->budget = $found; + + return true; + } + $name = $this->name['value'] ?? ''; + + if (strlen($name) === 0) { + return true; + } + + Log::debug('Found no budget so must create one ourselves.'); + + $data = [ + 'name' => $name, + ]; + + $this->budget = $this->repository->store($data); + Log::debug(sprintf('Successfully stored new budget #%d: %s', $this->budget->id, $this->budget->name)); + + return true; + } + + +} diff --git a/app/Import/Object/ImportCategory.php b/app/Import/Object/ImportCategory.php new file mode 100644 index 0000000000..b86a73d6ec --- /dev/null +++ b/app/Import/Object/ImportCategory.php @@ -0,0 +1,229 @@ +category = new Category(); + $this->repository = app(CategoryRepositoryInterface::class); + Log::debug('Created ImportCategory.'); + } + + /** + * @return Category + */ + public function getCategory(): Category + { + if (is_null($this->category->id)) { + $this->store(); + } + + return $this->category; + } + + /** + * @param array $id + */ + public function setId(array $id) + { + $this->id = $id; + } + + /** + * @param array $name + */ + public function setName(array $name) + { + $this->name = $name; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + $this->repository->setUser($user); + } + + /** + * @return Category + */ + private function findExistingObject(): Category + { + Log::debug('In findExistingObject() for Category'); + // 1: find by ID, or name + + if (count($this->id) === 3) { + Log::debug(sprintf('Finding category with ID #%d', $this->id['value'])); + /** @var Category $category */ + $category = $this->repository->find(intval($this->id['value'])); + if (!is_null($category->id)) { + Log::debug(sprintf('Found unmapped category by ID (#%d): %s', $category->id, $category->name)); + + return $category; + } + Log::debug('Found nothing.'); + } + // 2: find by name + if (count($this->name) === 3) { + /** @var Collection $categories */ + $categories = $this->repository->getCategories(); + $name = $this->name['value']; + Log::debug(sprintf('Finding category with name %s', $name)); + $filtered = $categories->filter( + function (Category $category) use ($name) { + if ($category->name === $name) { + Log::debug(sprintf('Found unmapped category by name (#%d): %s', $category->id, $category->name)); + + return $category; + } + + return null; + } + ); + + if ($filtered->count() === 1) { + return $filtered->first(); + } + Log::debug('Found nothing.'); + } + + // 4: do not search by account number. + Log::debug('Found NO existing categories.'); + + return new Category; + + } + + /** + * @return Category + */ + private function findMappedObject(): Category + { + Log::debug('In findMappedObject() for Category'); + $fields = ['id', 'name']; + foreach ($fields as $field) { + $array = $this->$field; + Log::debug(sprintf('Find mapped category based on field "%s" with value', $field), $array); + // check if a pre-mapped object exists. + $mapped = $this->getMappedObject($array); + if (!is_null($mapped->id)) { + Log::debug(sprintf('Found category #%d!', $mapped->id)); + + return $mapped; + } + + } + Log::debug('Found no category on mapped data or no map present.'); + + return new Category; + } + + /** + * @param array $array + * + * @return Category + */ + private function getMappedObject(array $array): Category + { + Log::debug('In getMappedObject() for Category'); + if (count($array) === 0) { + Log::debug('Array is empty, nothing will come of this.'); + + return new Category; + } + + if (array_key_exists('mapped', $array) && is_null($array['mapped'])) { + Log::debug(sprintf('No map present for value "%s". Return NULL.', $array['value'])); + + return new Category; + } + + Log::debug('Finding a mapped category based on', $array); + + $search = intval($array['mapped']); + $category = $this->repository->find($search); + + if (is_null($category->id)) { + Log::error(sprintf('There is no category with id #%d. Invalid mapping will be ignored!', $search)); + + return new Category; + } + + Log::debug(sprintf('Found category! #%d ("%s"). Return it', $category->id, $category->name)); + + return $category; + } + + /** + * @return bool + */ + private function store(): bool + { + // 1: find mapped object: + $mapped = $this->findMappedObject(); + if (!is_null($mapped->id)) { + $this->category = $mapped; + + return true; + } + // 2: find existing by given values: + $found = $this->findExistingObject(); + if (!is_null($found->id)) { + $this->category = $found; + + return true; + } + $name = $this->name['value'] ?? ''; + + if (strlen($name) === 0) { + return true; + } + + Log::debug('Found no category so must create one ourselves.'); + + $data = [ + 'name' => $name, + ]; + + $this->category = $this->repository->store($data); + Log::debug(sprintf('Successfully stored new category #%d: %s', $this->category->id, $this->category->name)); + + return true; + } + + +} diff --git a/app/Import/Object/ImportCurrency.php b/app/Import/Object/ImportCurrency.php new file mode 100644 index 0000000000..e282e29228 --- /dev/null +++ b/app/Import/Object/ImportCurrency.php @@ -0,0 +1,225 @@ +currency = new TransactionCurrency; + $this->repository = app(CurrencyRepositoryInterface::class); + } + + /** + * @return TransactionCurrency + */ + public function getTransactionCurrency(): TransactionCurrency + { + if (!is_null($this->currency->id)) { + return $this->currency; + } + Log::debug('In createCurrency()'); + // check if any of them is mapped: + $mapped = $this->findMappedObject(); + + if (!is_null($mapped->id)) { + + Log::debug('Mapped existing currency.', ['new' => $mapped->toArray()]); + $this->currency = $mapped; + + return $mapped; + } + + $searched = $this->findExistingObject(); + if (!is_null($searched->id)) { + Log::debug('Found existing currency.', ['found' => $searched->toArray()]); + $this->currency = $searched; + + return $searched; + } + $data = [ + 'code' => $this->code['value'] ?? null, + 'symbol' => $this->symbol['value'] ?? null, + 'name' => $this->name['value'] ?? null, + 'decimal_places' => 2, + ]; + if (is_null($data['code'])) { + Log::debug('Need at least a code to create currency, return nothing.'); + + return new TransactionCurrency(); + } + + Log::debug('Search for maps resulted in nothing, create new one based on', $data); + $currency = $this->repository->store($data); + $this->currency = $currency; + Log::info('Made new currency.', ['input' => $data, 'new' => $currency->toArray()]); + + + return $currency; + + } + + /** + * @param array $code + */ + public function setCode(array $code) + { + $this->code = $code; + } + + /** + * @param array $id + */ + public function setId(array $id) + { + $id['value'] = intval($id['value']); + $this->id = $id; + } + + /** + * @param array $name + */ + public function setName(array $name) + { + $this->name = $name; + } + + /** + * @param array $symbol + */ + public function setSymbol(array $symbol) + { + $this->symbol = $symbol; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + $this->repository->setUser($user); + } + + /** + * @return TransactionCurrency + */ + private function findExistingObject(): TransactionCurrency + { + $search = [ + 'id' => 'find', + 'code' => 'findByCode', + 'symbol' => 'findBySymbol', + 'name' => 'findByName', + ]; + foreach ($search as $field => $function) { + $value = $this->$field['value'] ?? null; + if (!is_null($value)) { + Log::debug(sprintf('Searching for %s using function %s and value %s', $field, $function, $value)); + $currency = $this->repository->$function($value); + + if (!is_null($currency->id)) { + return $currency; + } + } + } + + return new TransactionCurrency(); + } + + /** + * @return TransactionCurrency + */ + private function findMappedObject(): TransactionCurrency + { + Log::debug('In findMappedObject()'); + $fields = ['id', 'code', 'name', 'symbol']; + foreach ($fields as $field) { + $array = $this->$field; + Log::debug(sprintf('Find mapped currency based on field "%s" with value', $field), $array); + // check if a pre-mapped object exists. + $mapped = $this->getMappedObject($array); + if (!is_null($mapped->id)) { + Log::debug(sprintf('Found currency #%d!', $mapped->id)); + + return $mapped; + } + + } + Log::debug('Found no currency on mapped data or no map present.'); + + return new TransactionCurrency; + } + + /** + * @param array $array + * + * @return TransactionCurrency + */ + private function getMappedObject(array $array): TransactionCurrency + { + Log::debug('In getMappedObject()'); + if (count($array) === 0) { + Log::debug('Array is empty, nothing will come of this.'); + + return new TransactionCurrency; + } + + if (array_key_exists('mapped', $array) && is_null($array['mapped'])) { + Log::debug(sprintf('No map present for value "%s". Return NULL.', $array['value'])); + + return new TransactionCurrency; + } + + Log::debug('Finding a mapped object based on', $array); + + $search = intval($array['mapped']); + $currency = $this->repository->find($search); + + + if (is_null($currency->id)) { + Log::error(sprintf('There is no currency with id #%d. Invalid mapping will be ignored!', $search)); + + return new TransactionCurrency; + } + + Log::debug(sprintf('Found currency! #%d ("%s"). Return it', $currency->id, $currency->name)); + + return $currency; + } + + +} diff --git a/app/Import/Object/ImportJournal.php b/app/Import/Object/ImportJournal.php new file mode 100644 index 0000000000..c4aa0feaa3 --- /dev/null +++ b/app/Import/Object/ImportJournal.php @@ -0,0 +1,286 @@ +description === '') { + return '(no description)'; + } + + return $this->description; + } + + + /** + * ImportEntry constructor. + */ + public function __construct() + { + $this->errors = new Collection; + $this->asset = new ImportAccount; + $this->opposing = new ImportAccount; + $this->bill = new ImportBill; + $this->category = new ImportCategory; + $this->budget = new ImportBudget; + $this->currency = new ImportCurrency; + } + + /** + * @param array $modifier + */ + public function addToModifier(array $modifier) + { + $this->modifiers[] = $modifier; + } + + /** + * @return TransactionJournal + * @throws FireflyException + */ + public function createTransactionJournal(): TransactionJournal + { + exit('does not work yet'); + } + + /** + * @return string + */ + public function getAmount(): string + { + + /** @var ConverterInterface $amountConverter */ + $amountConverter = app(Amount::class); + $this->amount = $amountConverter->convert($this->amount); + // modify + foreach ($this->modifiers as $modifier) { + $class = sprintf('FireflyIII\Import\Converter\%s', config(sprintf('csv.import_roles.%s.converter', $modifier['role']))); + /** @var ConverterInterface $converter */ + $converter = app($class); + if ($converter->convert($modifier['value']) === -1) { + $this->amount = Steam::negative($this->amount); + } + } + + return $this->amount; + } + + /** + * @return ImportCurrency + */ + public function getCurrency(): ImportCurrency + { + return $this->currency; + } + + /** + * @param string $format + * + * @return Carbon + */ + public function getDate(string $format): Carbon + { + $date = new Carbon; + try { + $date = Carbon::createFromFormat($format, $this->date); + } catch (InvalidArgumentException $e) { + // don't care, just log. + Log::error(sprintf('Import journal cannot parse date "%s" from value "%s" so will return current date instead.', $format, $this->date)); + } + + return $date; + } + + /** + * @param string $hash + */ + public function setHash(string $hash) + { + $this->hash = $hash; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + + // set user for related objects: + $this->asset->setUser($user); + $this->opposing->setUser($user); + $this->budget->setUser($user); + $this->category->setUser($user); + $this->bill->setUser($user); + } + + /** + * @param array $array + * + * @throws FireflyException + */ + public function setValue(array $array) + { + switch ($array['role']) { + default: + throw new FireflyException(sprintf('ImportJournal cannot handle "%s" with value "%s".', $array['role'], $array['value'])); + case 'account-id': + $this->asset->setAccountId($array); + break; + case 'amount': + $this->amount = $array['value']; + break; + case 'account-iban': + $this->asset->setAccountIban($array); + break; + case 'account-name': + $this->asset->setAccountName($array); + break; + case 'account-number': + $this->asset->setAccountNumber($array); + break; + case 'bill-id': + $this->bill->setId($array); + break; + case 'bill-name': + $this->bill->setName($array); + break; + case 'budget-id': + $this->budget->setId($array); + break; + case 'budget-name': + $this->budget->setName($array); + break; + case 'category-id': + $this->category->setId($array); + break; + case 'category-name': + $this->category->setName($array); + break; + case 'currency-code': + $this->currency->setCode($array); + break; + case 'currency-id': + $this->currency->setId($array); + break; + case 'currency-name': + $this->currency->setName($array); + break; + case 'currency-symbol': + $this->currency->setSymbol($array); + break; + case 'date-transaction': + $this->date = $array['value']; + break; + case 'description': + $this->description .= $array['value']; + break; + case 'sepa-ct-op': + case 'sepa-ct-id': + case 'sepa-db': + $this->notes .= ' ' . $array['value']; + $this->notes = trim($this->notes); + break; + case 'external-id': + $this->externalId = $array['value']; + break; + case '_ignore': + break; + case 'ing-debet-credit': + case 'rabo-debet-credit': + $this->addToModifier($array); + break; + case 'opposing-iban': + $this->opposing->setAccountIban($array); + break; + case 'opposing-name': + $this->opposing->setAccountName($array); + break; + case 'opposing-number': + $this->opposing->setAccountNumber($array); + break; + case 'opposing-id': + $this->opposing->setAccountId($array); + break; + case 'tags-comma': + case 'tags-space': + $this->tags[] = $array; + break; + case 'date-interest': + $this->metaDates['interest_date'] = $array['value']; + break; + case 'date-book': + $this->metaDates['book_date'] = $array['value']; + break; + case 'date-process': + $this->metaDates['process_date'] = $array['value']; + break; + } + } +} diff --git a/app/Import/Routine/ImportRoutine.php b/app/Import/Routine/ImportRoutine.php new file mode 100644 index 0000000000..b032bc06d8 --- /dev/null +++ b/app/Import/Routine/ImportRoutine.php @@ -0,0 +1,176 @@ +journals = new Collection; + $this->errors = new Collection; + } + + /** + * + */ + public function run(): bool + { + if ($this->job->status !== 'configured') { + Log::error(sprintf('Job %s is in state "%s" so it cannot be started.', $this->job->key, $this->job->status)); + + return false; + } + set_time_limit(0); + Log::info(sprintf('Start with import job %s', $this->job->key)); + + $importObjects = $this->getImportObjects(); + $this->lines = $importObjects->count(); + + // once done, use storage thing to actually store them: + Log::info(sprintf('Returned %d valid objects from file processor', $this->lines)); + + $storage = $this->storeObjects($importObjects); + Log::debug('Back in run()'); + + // update job: + $this->job->status = 'finished'; + $this->job->save(); + + Log::debug('Updated job...'); + + $this->journals = $storage->journals; + $this->errors = $storage->errors; + + Log::debug('Going to call createImportTag()'); + + // create tag, link tag to all journals: + $this->createImportTag(); + + Log::info(sprintf('Done with import job %s', $this->job->key)); + + + return true; + } + + /** + * @param ImportJob $job + */ + public function setJob(ImportJob $job) + { + $this->job = $job; + } + + /** + * @return Collection + */ + protected function getImportObjects(): Collection + { + $objects = new Collection; + $type = $this->job->file_type; + $class = config(sprintf('firefly.import_processors.%s', $type)); + /** @var FileProcessorInterface $processor */ + $processor = app($class); + $processor->setJob($this->job); + + if ($this->job->status === 'configured') { + + // set job as "running"... + $this->job->status = 'running'; + $this->job->save(); + + Log::debug('Job is configured, start with run()'); + $processor->run(); + $objects = $processor->getObjects(); + } + + return $objects; + } + + /** + * + */ + private function createImportTag(): Tag + { + Log::debug('Now in createImportTag()'); + /** @var TagRepositoryInterface $repository */ + $repository = app(TagRepositoryInterface::class); + $repository->setUser($this->job->user); + $data = [ + 'tag' => trans('firefly.import_with_key', ['key' => $this->job->key]), + 'date' => new Carbon, + 'description' => null, + 'latitude' => null, + 'longitude' => null, + 'zoomLevel' => null, + 'tagMode' => 'nothing', + ]; + $tag = $repository->store($data); + $extended = $this->job->extended_status; + $extended['tag'] = $tag->id; + $this->job->extended_status = $extended; + $this->job->save(); + + Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag)); + Log::debug('Looping journals...'); + $journalIds = $this->journals->pluck('id')->toArray(); + $tagId = $tag->id; + foreach ($journalIds as $journalId) { + Log::debug(sprintf('Linking journal #%d to tag #%d...', $journalId, $tagId)); + DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journalId, 'tag_id' => $tagId]); + } + Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $this->journals->count(), $tag->id, $tag->tag)); + + return $tag; + } + + /** + * @param Collection $objects + * + * @return ImportStorage + */ + private function storeObjects(Collection $objects): ImportStorage + { + $storage = new ImportStorage; + $storage->setJob($this->job); + $storage->setDateFormat($this->job->configuration['date-format']); + $storage->setObjects($objects); + $storage->store(); + Log::info('Back in storeObjects()'); + + return $storage; + } +} diff --git a/app/Import/Setup/CsvSetup.php b/app/Import/Setup/CsvSetup.php deleted file mode 100644 index 51ccedb338..0000000000 --- a/app/Import/Setup/CsvSetup.php +++ /dev/null @@ -1,505 +0,0 @@ -defaultImportAccount = new Account; - } - - /** - * Create initial (empty) configuration array. - * - * @return bool - */ - public function configure(): bool - { - if (is_null($this->job->configuration) || (is_array($this->job->configuration) && count($this->job->configuration) === 0)) { - Log::debug('No config detected, will create empty one.'); - $this->job->configuration = config('csv.default_config'); - $this->job->save(); - - return true; - } - - // need to do nothing, for now. - Log::debug('Detected config in upload, will use that one. ', $this->job->configuration); - - return true; - } - - /** - * @return array - */ - public function getConfigurationData(): array - { - /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); - $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); - $delimiters = [ - ',' => trans('form.csv_comma'), - ';' => trans('form.csv_semicolon'), - 'tab' => trans('form.csv_tab'), - ]; - - $specifics = []; - - // collect specifics. - foreach (config('csv.import_specifics') as $name => $className) { - $specifics[$name] = [ - 'name' => $className::getName(), - 'description' => $className::getDescription(), - ]; - } - - $data = [ - 'accounts' => ExpandedForm::makeSelectList($accounts), - 'specifix' => [], - 'delimiters' => $delimiters, - 'upload_path' => storage_path('upload'), - 'is_upload_possible' => is_writable(storage_path('upload')), - 'specifics' => $specifics, - ]; - - return $data; - } - - /** - * This method returns the data required for the view that will let the user add settings to the import job. - * - * @return array - */ - public function getDataForSettings(): array - { - Log::debug('Now in getDataForSettings()'); - if ($this->doColumnRoles()) { - Log::debug('doColumnRoles() is true.'); - $data = $this->getDataForColumnRoles(); - - return $data; - } - - if ($this->doColumnMapping()) { - Log::debug('doColumnMapping() is true.'); - $data = $this->getDataForColumnMapping(); - - return $data; - } - - echo 'no settings to do.'; - exit; - - } - - /** - * This method returns the name of the view that will be shown to the user to further configure - * the import job. - * - * @return string - * @throws FireflyException - */ - public function getViewForSettings(): string - { - if ($this->doColumnRoles()) { - return 'import.csv.roles'; - } - - if ($this->doColumnMapping()) { - return 'import.csv.map'; - } - throw new FireflyException('There is no view for the current CSV import step.'); - } - - /** - * This method returns whether or not the user must configure this import - * job further. - * - * @return bool - */ - public function requireUserSettings(): bool - { - Log::debug(sprintf('doColumnMapping is %s', $this->doColumnMapping())); - Log::debug(sprintf('doColumnRoles is %s', $this->doColumnRoles())); - if ($this->doColumnMapping() || $this->doColumnRoles()) { - Log::debug('Return true'); - - return true; - } - Log::debug('Return false'); - - return false; - } - - /** - * @param array $data - * - * @param FileBag $files - * - * @return bool - */ - public function saveImportConfiguration(array $data, FileBag $files): bool - { - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $importId = $data['csv_import_account'] ?? 0; - $account = $repository->find(intval($importId)); - - $hasHeaders = isset($data['has_headers']) && intval($data['has_headers']) === 1 ? true : false; - $config = $this->job->configuration; - $config['has-headers'] = $hasHeaders; - $config['date-format'] = $data['date_format']; - $config['delimiter'] = $data['csv_delimiter']; - $config['delimiter'] = $config['delimiter'] === 'tab' ? "\t" : $config['delimiter']; - - Log::debug('Entered import account.', ['id' => $importId]); - - - if (!is_null($account->id)) { - Log::debug('Found account.', ['id' => $account->id, 'name' => $account->name]); - $config['import-account'] = $account->id; - } else { - Log::error('Could not find anything for csv_import_account.', ['id' => $importId]); - } - // loop specifics. - if (isset($data['specifics']) && is_array($data['specifics'])) { - foreach ($data['specifics'] as $name => $enabled) { - // verify their content. - $className = sprintf('FireflyIII\Import\Specifics\%s', $name); - if (class_exists($className)) { - $config['specifics'][$name] = 1; - } - } - } - $this->job->configuration = $config; - $this->job->save(); - - return true; - - - } - - /** - * @param ImportJob $job - */ - public function setJob(ImportJob $job) - { - $this->job = $job; - } - - /** - * Store the settings filled in by the user, if applicable. - * - * @param Request $request - * - */ - public function storeSettings(Request $request) - { - $config = $this->job->configuration; - $all = $request->all(); - if ($request->get('settings') == 'roles') { - $count = $config['column-count']; - - $roleSet = 0; // how many roles have been defined - $mapSet = 0; // how many columns must be mapped - for ($i = 0; $i < $count; $i++) { - $selectedRole = $all['role'][$i] ?? '_ignore'; - $doMapping = isset($all['map'][$i]) && $all['map'][$i] == '1' ? true : false; - if ($selectedRole == '_ignore' && $doMapping === true) { - $doMapping = false; // cannot map ignored columns. - } - if ($selectedRole != '_ignore') { - $roleSet++; - } - if ($doMapping === true) { - $mapSet++; - } - $config['column-roles'][$i] = $selectedRole; - $config['column-do-mapping'][$i] = $doMapping; - } - if ($roleSet > 0) { - $config['column-roles-complete'] = true; - $this->job->configuration = $config; - $this->job->save(); - } - if ($mapSet === 0) { - // skip setting of map: - $config['column-mapping-complete'] = true; - } - } - if ($request->get('settings') == 'map') { - if (isset($all['mapping'])) { - foreach ($all['mapping'] as $index => $data) { - $config['column-mapping-config'][$index] = []; - foreach ($data as $value => $mapId) { - $mapId = intval($mapId); - if ($mapId !== 0) { - $config['column-mapping-config'][$index][$value] = intval($mapId); - } - } - } - } - - // set thing to be completed. - $config['column-mapping-complete'] = true; - $this->job->configuration = $config; - $this->job->save(); - } - } - - /** - * @return bool - */ - private function doColumnMapping(): bool - { - $mapArray = $this->job->configuration['column-do-mapping'] ?? []; - $doMap = false; - foreach ($mapArray as $value) { - if ($value === true) { - $doMap = true; - break; - } - } - - return $this->job->configuration['column-mapping-complete'] === false && $doMap; - } - - /** - * @return bool - */ - private function doColumnRoles(): bool - { - return $this->job->configuration['column-roles-complete'] === false; - } - - /** - * @return array - * @throws FireflyException - */ - private function getDataForColumnMapping(): array - { - $config = $this->job->configuration; - $data = []; - $indexes = []; - - foreach ($config['column-do-mapping'] as $index => $mustBeMapped) { - if ($mustBeMapped) { - - $column = $config['column-roles'][$index] ?? '_ignore'; - - // is valid column? - $validColumns = array_keys(config('csv.import_roles')); - if (!in_array($column, $validColumns)) { - throw new FireflyException(sprintf('"%s" is not a valid column.', $column)); - } - - $canBeMapped = config('csv.import_roles.' . $column . '.mappable'); - $preProcessMap = config('csv.import_roles.' . $column . '.pre-process-map'); - if ($canBeMapped) { - $mapperClass = config('csv.import_roles.' . $column . '.mapper'); - $mapperName = sprintf('\\FireflyIII\\Import\Mapper\\%s', $mapperClass); - /** @var MapperInterface $mapper */ - $mapper = new $mapperName; - $indexes[] = $index; - $data[$index] = [ - 'name' => $column, - 'mapper' => $mapperName, - 'index' => $index, - 'options' => $mapper->getMap(), - 'preProcessMap' => null, - 'values' => [], - ]; - if ($preProcessMap) { - $preClass = sprintf( - '\\FireflyIII\\Import\\MapperPreProcess\\%s', - config('csv.import_roles.' . $column . '.pre-process-mapper') - ); - $data[$index]['preProcessMap'] = $preClass; - } - } - - } - } - - // in order to actually map we also need all possible values from the CSV file. - $content = $this->job->uploadFileContents(); - /** @var Reader $reader */ - $reader = Reader::createFromString($content); - $reader->setDelimiter($config['delimiter']); - $results = $reader->fetch(); - $validSpecifics = array_keys(config('csv.import_specifics')); - - foreach ($results as $rowIndex => $row) { - - // skip first row? - if ($rowIndex === 0 && $config['has-headers']) { - continue; - } - - // run specifics here: - // and this is the point where the specifix go to work. - foreach ($config['specifics'] as $name => $enabled) { - - if (!in_array($name, $validSpecifics)) { - throw new FireflyException(sprintf('"%s" is not a valid class name', $name)); - } - $class = config('csv.import_specifics.' . $name); - /** @var SpecificInterface $specific */ - $specific = app($class); - - // it returns the row, possibly modified: - $row = $specific->run($row); - } - - //do something here - foreach ($indexes as $index) { // this is simply 1, 2, 3, etc. - if (!isset($row[$index])) { - // don't really know how to handle this. Just skip, for now. - continue; - } - $value = $row[$index]; - if (strlen($value) > 0) { - - // we can do some preprocessing here, - // which is exclusively to fix the tags: - if (!is_null($data[$index]['preProcessMap'])) { - /** @var PreProcessorInterface $preProcessor */ - $preProcessor = app($data[$index]['preProcessMap']); - $result = $preProcessor->run($value); - $data[$index]['values'] = array_merge($data[$index]['values'], $result); - - Log::debug($rowIndex . ':' . $index . 'Value before preprocessor', ['value' => $value]); - Log::debug($rowIndex . ':' . $index . 'Value after preprocessor', ['value-new' => $result]); - Log::debug($rowIndex . ':' . $index . 'Value after joining', ['value-complete' => $data[$index]['values']]); - - - continue; - } - - $data[$index]['values'][] = $value; - } - } - } - foreach ($data as $index => $entry) { - $data[$index]['values'] = array_unique($data[$index]['values']); - } - - return $data; - } - - /** - * This method collects the data that will enable a user to choose column content. - * - * @return array - */ - private function getDataForColumnRoles(): array - { - Log::debug('Now in getDataForColumnRoles()'); - $config = $this->job->configuration; - $data = [ - 'columns' => [], - 'columnCount' => 0, - 'columnHeaders' => [], - ]; - - // show user column role configuration. - $content = $this->job->uploadFileContents(); - - // create CSV reader. - $reader = Reader::createFromString($content); - $reader->setDelimiter($config['delimiter']); - $start = $config['has-headers'] ? 1 : 0; - $end = $start + config('csv.example_rows'); - $header = []; - if ($config['has-headers']) { - $header = $reader->fetchOne(0); - } - - - // collect example data in $data['columns'] - Log::debug(sprintf('While %s is smaller than %d', $start, $end)); - while ($start < $end) { - $row = $reader->fetchOne($start); - Log::debug(sprintf('Row %d has %d columns', $start, count($row))); - // run specifics here: - // and this is the point where the specifix go to work. - foreach ($config['specifics'] as $name => $enabled) { - /** @var SpecificInterface $specific */ - $specific = app('FireflyIII\Import\Specifics\\' . $name); - Log::debug(sprintf('Will now apply specific "%s" to row %d.', $name, $start)); - // it returns the row, possibly modified: - $row = $specific->run($row); - } - - foreach ($row as $index => $value) { - $value = trim($value); - $data['columnHeaders'][$index] = $header[$index] ?? ''; - if (strlen($value) > 0) { - $data['columns'][$index][] = $value; - } - } - $start++; - $data['columnCount'] = count($row) > $data['columnCount'] ? count($row) : $data['columnCount']; - } - - // make unique example data - foreach ($data['columns'] as $index => $values) { - $data['columns'][$index] = array_unique($values); - } - - $data['set_roles'] = []; - // collect possible column roles: - $data['available_roles'] = []; - foreach (array_keys(config('csv.import_roles')) as $role) { - $data['available_roles'][$role] = trans('csv.column_' . $role); - } - - $config['column-count'] = $data['columnCount']; - $this->job->configuration = $config; - $this->job->save(); - - return $data; - - - } -} diff --git a/app/Import/Setup/SetupInterface.php b/app/Import/Setup/SetupInterface.php deleted file mode 100644 index cffae80eba..0000000000 --- a/app/Import/Setup/SetupInterface.php +++ /dev/null @@ -1,88 +0,0 @@ -row[8] = $matches[4]; // 'opposing-account-name' $this->row[7] = $matches[4]; // 'description' - if ($matches[1] == 'GEA') { + if ($matches[1] === 'GEA') { $this->row[7] = 'GEA ' . $matches[4]; // 'description' } diff --git a/app/Import/Storage/ImportStorage.php b/app/Import/Storage/ImportStorage.php new file mode 100644 index 0000000000..964fafa1f7 --- /dev/null +++ b/app/Import/Storage/ImportStorage.php @@ -0,0 +1,493 @@ +objects = new Collection; + $this->journals = new Collection; + $this->errors = new Collection; + } + + /** + * @param string $dateFormat + */ + public function setDateFormat(string $dateFormat) + { + $this->dateFormat = $dateFormat; + } + + /** + * @param ImportJob $job + */ + public function setJob(ImportJob $job) + { + $this->job = $job; + $repository = app(CurrencyRepositoryInterface::class); + $repository->setUser($job->user); + $this->currencyRepository = $repository; + $this->rules = $this->getUserRules(); + } + + /** + * @param Collection $objects + */ + public function setObjects(Collection $objects) + { + $this->objects = $objects; + } + + /** + * Do storage of import objects + */ + public function store() + { + $this->defaultCurrency = Amount::getDefaultCurrencyByUser($this->job->user); + + // routine below consists of 3 steps. + /** + * @var int $index + * @var ImportJournal $object + */ + foreach ($this->objects as $index => $object) { + try { + $this->storeImportJournal($index, $object); + } catch (FireflyException $e) { + $this->errors->push($e->getMessage()); + Log::error(sprintf('Cannot import row #%d because: %s', $index, $e->getMessage())); + } + } + Log::info('ImportStorage has finished.'); + + return true; + } + + /** + * @param TransactionJournal $journal + * + * @return bool + */ + protected function applyRules(TransactionJournal $journal): bool + { + if ($this->rules->count() > 0) { + + /** @var Rule $rule */ + foreach ($this->rules as $rule) { + Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id)); + $processor = Processor::make($rule); + $processor->handleTransactionJournal($journal); + + if ($rule->stop_processing) { + return true; + } + } + } + + return true; + } + + /** + * @param int $journalId + * @param int $accountId + * @param int $currencyId + * @param string $amount + * + * @return bool + * @throws FireflyException + */ + private function createTransaction(int $journalId, int $accountId, int $currencyId, string $amount): bool + { + $transaction = new Transaction; + $transaction->account_id = $accountId; + $transaction->transaction_journal_id = $journalId; + $transaction->transaction_currency_id = $currencyId; + $transaction->amount = $amount; + $transaction->save(); + if (is_null($transaction->id)) { + $errorText = join(', ', $transaction->getErrors()->all()); + throw new FireflyException($errorText); + } + Log::debug(sprintf('Created transaction with ID #%d, account #%d, amount %s', $transaction->id, $accountId, $amount)); + + return true; + } + + /** + * @param ImportJournal $importJournal + * + * @return TransactionCurrency + */ + private function getCurrency(ImportJournal $importJournal, Account $account): TransactionCurrency + { + // start with currency pref of account, if any: + $currency = $this->currencyRepository->find(intval($account->getMeta('currency_id'))); + if (!is_null($currency->id)) { + return $currency; + } + + // use given currency + $currency = $importJournal->getCurrency()->getTransactionCurrency(); + if (!is_null($currency->id)) { + return $currency; + } + + // backup to default + $currency = $this->defaultCurrency; + + return $currency; + } + + /** + * @param ImportAccount $account + * @param $amount + * + * @return Account + */ + private function getOpposingAccount(ImportAccount $account, $amount): Account + { + if (bccomp($amount, '0') === -1) { + Log::debug(sprintf('%s is negative, create opposing expense account.', $amount)); + $account->setExpectedType(AccountType::EXPENSE); + + return $account->getAccount(); + } + Log::debug(sprintf('%s is positive, create opposing revenue account.', $amount)); + // amount is positive, it's a deposit, opposing is an revenue: + $account->setExpectedType(AccountType::REVENUE); + + $databaseAccount = $account->getAccount(); + + return $databaseAccount; + + } + + /** + * @param string $amount + * + * @return TransactionType + */ + private function getTransactionType(string $amount): TransactionType + { + $transactionType = new TransactionType(); + // amount is negative, it's a withdrawal, opposing is an expense: + if (bccomp($amount, '0') === -1) { + $transactionType = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + } + if (bccomp($amount, '0') === 1) { + $transactionType = TransactionType::whereType(TransactionType::DEPOSIT)->first(); + } + + return $transactionType; + } + + /** + * @return Collection + */ + private function getUserRules(): Collection + { + $set = Rule::distinct() + ->where('rules.user_id', $this->job->user->id) + ->leftJoin('rule_groups', 'rule_groups.id', '=', 'rules.rule_group_id') + ->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id') + ->where('rule_groups.active', 1) + ->where('rule_triggers.trigger_type', 'user_action') + ->where('rule_triggers.trigger_value', 'store-journal') + ->where('rules.active', 1) + ->orderBy('rule_groups.order', 'ASC') + ->orderBy('rules.order', 'ASC') + ->get(['rules.*', 'rule_groups.order']); + Log::debug(sprintf('Found %d user rules.', $set->count())); + + return $set; + + } + + /** + * @param TransactionJournal $journal + * @param Bill $bill + */ + private function storeBill(TransactionJournal $journal, Bill $bill) + { + if (!is_null($bill->id)) { + Log::debug(sprintf('Linked bill #%d to journal #%d', $bill->id, $journal->id)); + $journal->bill()->associate($bill); + $journal->save(); + } + } + + /** + * @param TransactionJournal $journal + * @param Budget $budget + */ + private function storeBudget(TransactionJournal $journal, Budget $budget) + { + if (!is_null($budget->id)) { + Log::debug(sprintf('Linked budget #%d to journal #%d', $budget->id, $journal->id)); + $journal->budgets()->save($budget); + } + } + + /** + * @param TransactionJournal $journal + * @param Category $category + */ + private function storeCategory(TransactionJournal $journal, Category $category) + { + + if (!is_null($category->id)) { + Log::debug(sprintf('Linked category #%d to journal #%d', $category->id, $journal->id)); + $journal->categories()->save($category); + } + + } + + private function storeImportJournal(int $index, ImportJournal $importJournal): bool + { + Log::debug(sprintf('Going to store object #%d with description "%s"', $index, $importJournal->getDescription())); + $importJournal->asset->setDefaultAccountId($this->job->configuration['import-account']); + $asset = $importJournal->asset->getAccount(); + $amount = $importJournal->getAmount(); + $currency = $this->getCurrency($importJournal, $asset); + $date = $importJournal->getDate($this->dateFormat); + $transactionType = $this->getTransactionType($amount); + $opposing = $this->getOpposingAccount($importJournal->opposing, $amount); + + // if opposing is an asset account, it's a transfer: + if ($opposing->accountType->type === AccountType::ASSET) { + Log::debug(sprintf('Opposing account #%d %s is an asset account, make transfer.', $opposing->id, $opposing->name)); + $transactionType = TransactionType::whereType(TransactionType::TRANSFER)->first(); + } + + // verify that opposing account is of the correct type: + if ($opposing->accountType->type === AccountType::EXPENSE && $transactionType->type !== TransactionType::WITHDRAWAL) { + Log::error(sprintf('Row #%d is imported as a %s but opposing is an expense account. This cannot be!', $index, $transactionType->type)); + } + + /*** First step done! */ + $this->job->addStepsDone(1); + + // could be that transfer is double: verify this. + if ($this->verifyDoubleTransfer($transactionType, $importJournal)) { + // add three steps: + $this->job->addStepsDone(3); + // throw error + throw new FireflyException('Detected a possible duplicate, skip this one.'); + + } + + // create a journal: + $journal = new TransactionJournal; + $journal->user_id = $this->job->user_id; + $journal->transaction_type_id = $transactionType->id; + $journal->transaction_currency_id = $currency->id; + $journal->description = $importJournal->getDescription(); + $journal->date = $date->format('Y-m-d'); + $journal->order = 0; + $journal->tag_count = 0; + $journal->completed = false; + + if (!$journal->save()) { + $errorText = join(', ', $journal->getErrors()->all()); + // add three steps: + $this->job->addStepsDone(3); + // throw error + throw new FireflyException($errorText); + } + + // save meta data: + $journal->setMeta('importHash', $importJournal->hash); + Log::debug(sprintf('Created journal with ID #%d', $journal->id)); + + // create transactions: + $this->createTransaction($journal->id, $asset->id, $currency->id, $amount); + $this->createTransaction($journal->id, $opposing->id, $currency->id, Steam::opposite($amount)); + + /*** Another step done! */ + $this->job->addStepsDone(1); + + // store meta object things: + $this->storeCategory($journal, $importJournal->category->getCategory()); + $this->storeBudget($journal, $importJournal->budget->getBudget()); + $this->storeBill($journal, $importJournal->bill->getBill()); + $this->storeMeta($journal, $importJournal->metaDates); + + // sepa thing as note: + if (strlen($importJournal->notes) > 0) { + $journal->setMeta('notes', $importJournal->notes); + } + + // set journal completed: + $journal->completed = true; + $journal->save(); + + $this->job->addStepsDone(1); + + // run rules: + $this->applyRules($journal); + $this->job->addStepsDone(1); + $this->journals->push($journal); + + Log::info( + sprintf( + 'Imported new journal #%d with description "%s" and amount %s %s.', $journal->id, $journal->description, $journal->transactionCurrency->code, + $amount + ) + ); + + return true; + } + + /** + * @param TransactionJournal $journal + * @param array $dates + */ + private function storeMeta(TransactionJournal $journal, array $dates) + { + // all other date fields as meta thing: + foreach ($dates as $name => $value) { + try { + $date = new Carbon($value); + $journal->setMeta($name, $date); + } catch (\Exception $e) { + // don't care, ignore: + Log::warning(sprintf('Could not parse "%s" into a valid Date object for field %s', $value, $name)); + } + } + } + + /** + * This method checks if the given transaction is a transfer and if so, if it might be a duplicate of an already imported transfer. + * This is important for import files that cover multiple accounts (and include both A<>B and B<>A transactions). + * + * @param TransactionType $transactionType + * @param ImportJournal $importJournal + * + * @return bool + */ + private function verifyDoubleTransfer(TransactionType $transactionType, ImportJournal $importJournal): bool + { + if ($transactionType->type === TransactionType::TRANSFER) { + $amount = Steam::positive($importJournal->getAmount()); + $asset = $importJournal->asset->getAccount(); + $opposing = $this->getOpposingAccount($importJournal->opposing, $amount); + $date = $importJournal->getDate($this->dateFormat); + $description = $importJournal->getDescription(); + $set = TransactionJournal:: + leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->leftJoin( + 'transactions AS source', function (JoinClause $join) { + $join->on('transaction_journals.id', '=', 'source.transaction_journal_id')->where('source.amount', '<', 0); + } + ) + ->leftJoin( + 'transactions AS destination', function (JoinClause $join) { + $join->on('transaction_journals.id', '=', 'destination.transaction_journal_id')->where( + 'destination.amount', '>', 0 + ); + } + ) + ->leftJoin('accounts as source_accounts', 'source.account_id', '=', 'source_accounts.id') + ->leftJoin('accounts as destination_accounts', 'destination.account_id', '=', 'destination_accounts.id') + ->where('transaction_journals.user_id', $this->job->user_id) + ->where('transaction_types.type', TransactionType::TRANSFER) + ->where('transaction_journals.date', $date->format('Y-m-d')) + ->where('destination.amount', $amount) + ->get( + ['transaction_journals.id', 'transaction_journals.encrypted', 'transaction_journals.description', + 'source_accounts.name as source_name', 'destination_accounts.name as destination_name'] + ); + if ($set->count() > 0) { + $filtered = $set->filter( + function (TransactionJournal $journal) use ($asset, $opposing, $description) { + $match = true; + $compare = [$asset->name, $opposing->name]; + + if ($journal->description !== $description) { + $match = false; + } + // when both are in array match is true. So reverse: + if (!(in_array(app('steam')->tryDecrypt($journal->source_name), $compare) + && in_array( + app('steam')->tryDecrypt($journal->destination_name), $compare + )) + ) { + $match = false; + } + + if ($match) { + return $journal; + } + + return null; + } + ); + if (count($filtered) > 0) { + return true; + } + } + } + + + return false; + } + +} diff --git a/app/Import/notes.txt b/app/Import/notes.txt deleted file mode 100644 index 603e185f84..0000000000 --- a/app/Import/notes.txt +++ /dev/null @@ -1,47 +0,0 @@ -The import routine is as follows: - -1. Upload and setup: - -User uploads a file with entries. The Setup/SetupInterface gives the user -the opportunity (in any number of steps) to do the necessary configuration. -This could also be skipped of course. An ImportJob object is created with a -basic and empty configuration. - -Helper classes are as follows, greatly modelled to the CSV importer: - -- The Mapper classes give back lists of Firefly objects. You can show them to the -user in order to help you convert text values to their Firefly counterparts. -For example, the user maps "Gcrsr" to category Groceries. -- The Converter classes exist to help convert text values to their Firely counterparts. -Feed "AB12ABCD897829" to the AssetAccountIban Converter and you should end up with a new -or found asset account. The previously built mapping is used to narrow it down. Submit an empty -mapping if this one is not relevant. - -The mapping and possibly other configuration options are stored in a newly created -ImportJob object, stored in the database. This import job holds a reference to the uploaded file -(placed encrypted in /storage/uploads) and the status of the import. - -2. Actual import - -Using either the command line or the web interface the user can tell Firefly to start the import. - -The ImporterInterface runs and creates an ImportEntry for each line, blob or whatever distinction it -wants. - -For each line, the ImporterInterface should run each field through the selected Converter in order -to convert the text values to their Firefly counterparts. Again, this is modelled to the CSV importer -and may need an update for MT940. - -In any case, this newly minted set of ImportEntries (it cannot be saved or stored atm, -this has to be done in one go) is then fed to the ImportValidator which will reject entries -(almost never) and corrects fields if necessary. - - -- Adds the date (today) if no date is present. -- Determins the type of transaction (withdrawal, deposit, transfer). -- Determins the types of accounts involved (asset, expense, revenue). -- Determins the currency of the transaction. -- Adds a default description if there isn't one present. - -This set of corrected ImportEntries is then fed to the ImportStorage class which will generate -TransactionJournals, Transactions and other related objects. \ No newline at end of file diff --git a/app/Jobs/MailError.php b/app/Jobs/MailError.php index 05de2aca0a..fd4a0164ff 100644 --- a/app/Jobs/MailError.php +++ b/app/Jobs/MailError.php @@ -80,7 +80,7 @@ class MailError extends Job implements ShouldQueue Mail::send( ['emails.error-html', 'emails.error-text'], $args, function (Message $message) use ($email) { - if ($email != 'mail@example.com') { + if ($email !== 'mail@example.com') { $message->to($email, $email)->subject('Caught an error in Firely III'); } } diff --git a/app/Mail/RegisteredUser.php b/app/Mail/RegisteredUser.php index f48ddfe6d5..ea8e6a7e63 100644 --- a/app/Mail/RegisteredUser.php +++ b/app/Mail/RegisteredUser.php @@ -12,17 +12,18 @@ class RegisteredUser extends Mailable /** @var string */ public $address; /** @var string */ - public $ip; + public $ipAddress; /** * Create a new message instance. * - * @return void + * @param string $address + * @param string $ipAddress */ - public function __construct(string $address, string $ip) + public function __construct(string $address, string $ipAddress) { - $this->address = $address; - $this->ip = $ip; + $this->address = $address; + $this->ipAddress = $ipAddress; } /** diff --git a/app/Mail/RequestedNewPassword.php b/app/Mail/RequestedNewPassword.php index b97a47c50f..bd2d9e90b0 100644 --- a/app/Mail/RequestedNewPassword.php +++ b/app/Mail/RequestedNewPassword.php @@ -10,7 +10,7 @@ class RequestedNewPassword extends Mailable { use Queueable, SerializesModels; /** @var string */ - public $ip; + public $ipAddress; /** @var string */ public $url; @@ -18,12 +18,12 @@ class RequestedNewPassword extends Mailable * RequestedNewPassword constructor. * * @param string $url - * @param string $ip + * @param string $ipAddress */ - public function __construct(string $url, string $ip) + public function __construct(string $url, string $ipAddress) { - $this->url = $url; - $this->ip = $ip; + $this->url = $url; + $this->ipAddress = $ipAddress; } /** diff --git a/app/Models/Account.php b/app/Models/Account.php index cfef7e34af..f6b4481c1d 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -95,7 +95,7 @@ class Account extends Model /** @var Account $account */ foreach ($set as $account) { - if ($account->name == $fields['name']) { + if ($account->name === $fields['name']) { return $account; } } @@ -116,7 +116,7 @@ class Account extends Model { if (auth()->check()) { - if ($value->user_id == auth()->user()->id) { + if ($value->user_id === auth()->user()->id) { return $value; } } @@ -187,7 +187,7 @@ class Account extends Model public function getMeta(string $fieldName): string { foreach ($this->accountMeta as $meta) { - if ($meta->name == $fieldName) { + if ($meta->name === $fieldName) { return strval($meta->data); } } diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php index ddca8ad0d1..ce8eeec9fc 100644 --- a/app/Models/Attachment.php +++ b/app/Models/Attachment.php @@ -55,7 +55,7 @@ class Attachment extends Model { if (auth()->check()) { - if ($value->user_id == auth()->user()->id) { + if ($value->user_id === auth()->user()->id) { return $value; } } diff --git a/app/Models/Bill.php b/app/Models/Bill.php index 4a858720e5..d879cbd8d3 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -62,7 +62,7 @@ class Bill extends Model public static function routeBinder(Bill $value) { if (auth()->check()) { - if ($value->user_id == auth()->user()->id) { + if ($value->user_id === auth()->user()->id) { return $value; } } @@ -77,7 +77,7 @@ class Bill extends Model public function getMatchAttribute($value) { - if (intval($this->match_encrypted) == 1) { + if (intval($this->match_encrypted) === 1) { return Crypt::decrypt($value); } @@ -92,7 +92,7 @@ class Bill extends Model public function getNameAttribute($value) { - if (intval($this->name_encrypted) == 1) { + if (intval($this->name_encrypted) === 1) { return Crypt::decrypt($value); } diff --git a/app/Models/Budget.php b/app/Models/Budget.php index d5f71f21ee..cbbaafab24 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -66,7 +66,7 @@ class Budget extends Model $set = $query->get(['budgets.*']); /** @var Budget $budget */ foreach ($set as $budget) { - if ($budget->name == $fields['name']) { + if ($budget->name === $fields['name']) { return $budget; } } @@ -85,7 +85,7 @@ class Budget extends Model public static function routeBinder(Budget $value) { if (auth()->check()) { - if ($value->user_id == auth()->user()->id) { + if ($value->user_id === auth()->user()->id) { return $value; } } diff --git a/app/Models/Category.php b/app/Models/Category.php index fd7241702f..e10e384d35 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -67,7 +67,7 @@ class Category extends Model $set = $query->get(['categories.*']); /** @var Category $category */ foreach ($set as $category) { - if ($category->name == $fields['name']) { + if ($category->name === $fields['name']) { return $category; } } @@ -86,7 +86,7 @@ class Category extends Model public static function routeBinder(Category $value) { if (auth()->check()) { - if ($value->user_id == auth()->user()->id) { + if ($value->user_id === auth()->user()->id) { return $value; } } diff --git a/app/Models/CurrencyExchangeRate.php b/app/Models/CurrencyExchangeRate.php index 2c7d12558c..bfd66f881f 100644 --- a/app/Models/CurrencyExchangeRate.php +++ b/app/Models/CurrencyExchangeRate.php @@ -50,4 +50,4 @@ class CurrencyExchangeRate extends Model return $this->belongsTo(User::class); } -} \ No newline at end of file +} diff --git a/app/Models/ImportJob.php b/app/Models/ImportJob.php index 66dfc167eb..dea6e581c9 100644 --- a/app/Models/ImportJob.php +++ b/app/Models/ImportJob.php @@ -42,11 +42,11 @@ class ImportJob extends Model protected $validStatus = [ - 'import_status_never_started', // initial state - 'import_configuration_saved', // import configuration saved. This step is going to be obsolete. - 'settings_complete', // aka: ready for import. - 'import_running', // import currently underway - 'import_complete', // done with everything + 'new', + 'initialized', + 'configured', + 'running', + 'finished', ]; /** @@ -66,13 +66,28 @@ class ImportJob extends Model throw new NotFoundHttpException; } + /** + * @param int $index + * @param string $message + * + * @return bool + */ + public function addError(int $index, string $message): bool + { + $extended = $this->extended_status; + $extended['errors'][$index][] = $message; + $this->extended_status = $extended; + + return true; + } + /** * @param int $count */ public function addStepsDone(int $count) { $status = $this->extended_status; - $status['steps_done'] += $count; + $status['done'] += $count; $this->extended_status = $status; $this->save(); @@ -84,7 +99,7 @@ class ImportJob extends Model public function addTotalSteps(int $count) { $status = $this->extended_status; - $status['total_steps'] += $count; + $status['steps'] += $count; $this->extended_status = $status; $this->save(); @@ -109,7 +124,7 @@ class ImportJob extends Model if (is_null($value)) { return []; } - if (strlen($value) == 0) { + if (strlen($value) === 0) { return []; } @@ -123,7 +138,7 @@ class ImportJob extends Model */ public function getExtendedStatusAttribute($value) { - if (strlen($value) == 0) { + if (strlen($value) === 0) { return []; } @@ -165,7 +180,7 @@ class ImportJob extends Model $disk = Storage::disk('upload'); $encryptedContent = $disk->get($fileName); $content = Crypt::decrypt($encryptedContent); - Log::debug(sprintf('Content size is %d bytes.', $content)); + Log::debug(sprintf('Content size is %d bytes.', strlen($content))); return $content; } diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index e648623ba4..e7659a80e8 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -58,7 +58,7 @@ class PiggyBank extends Model public static function routeBinder(PiggyBank $value) { if (auth()->check()) { - if ($value->account->user_id == auth()->user()->id) { + if ($value->account->user_id === auth()->user()->id) { return $value; } } diff --git a/app/Models/Rule.php b/app/Models/Rule.php index 28637d2893..59d572c424 100644 --- a/app/Models/Rule.php +++ b/app/Models/Rule.php @@ -51,7 +51,7 @@ class Rule extends Model public static function routeBinder(Rule $value) { if (auth()->check()) { - if ($value->user_id == auth()->user()->id) { + if ($value->user_id === auth()->user()->id) { return $value; } } diff --git a/app/Models/RuleGroup.php b/app/Models/RuleGroup.php index 8ab7f6a117..1b62d9a1fe 100644 --- a/app/Models/RuleGroup.php +++ b/app/Models/RuleGroup.php @@ -52,7 +52,7 @@ class RuleGroup extends Model public static function routeBinder(RuleGroup $value) { if (auth()->check()) { - if ($value->user_id == auth()->user()->id) { + if ($value->user_id === auth()->user()->id) { return $value; } } diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 30018b5e87..4ac3c24356 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -66,7 +66,7 @@ class Tag extends Model $set = $query->get(['tags.*']); /** @var Tag $tag */ foreach ($set as $tag) { - if ($tag->tag == $fields['tag']) { + if ($tag->tag === $fields['tag']) { return $tag; } } @@ -87,7 +87,7 @@ class Tag extends Model public static function routeBinder(Tag $value) { if (auth()->check()) { - if ($value->user_id == auth()->user()->id) { + if ($value->user_id === auth()->user()->id) { return $value; } } @@ -133,6 +133,10 @@ class Tag extends Model */ public function getTagAttribute($value) { + if (is_null($value)) { + return null; + } + return Crypt::decrypt($value); } diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index ef5deeadcd..c9b139d485 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -26,14 +26,13 @@ use Watson\Validating\ValidatingTrait; */ class Transaction extends Model { - /** * The attributes that should be casted to native types. * * @var array */ protected $casts - = [ + = [ 'created_at' => 'date', 'updated_at' => 'date', 'deleted_at' => 'date', @@ -41,17 +40,19 @@ class Transaction extends Model 'encrypted' => 'boolean', // model does not have these fields though 'bill_name_encrypted' => 'boolean', ]; - protected $dates = ['created_at', 'updated_at', 'deleted_at']; - protected $fillable = ['account_id', 'transaction_journal_id', 'description', 'amount', 'identifier']; - protected $hidden = ['encrypted']; + protected $dates = ['created_at', 'updated_at', 'deleted_at']; + protected $fillable + = ['account_id', 'transaction_journal_id', 'description', 'amount', 'identifier', 'transaction_currency_id', 'foreign_currency_id', + 'foreign_amount']; + protected $hidden = ['encrypted']; protected $rules - = [ - 'account_id' => 'required|exists:accounts,id', - 'transaction_journal_id' => 'required|exists:transaction_journals,id', - 'description' => 'between:0,1024', - 'amount' => 'required|numeric', + = [ + 'account_id' => 'required|exists:accounts,id', + 'transaction_journal_id' => 'required|exists:transaction_journals,id', + 'transaction_currency_id' => 'required|exists:transaction_currencies,id', + 'description' => 'between:0,1024', + 'amount' => 'required|numeric', ]; - use SoftDeletes, ValidatingTrait; /** * @param Builder $query @@ -74,6 +75,8 @@ class Transaction extends Model return false; } + use SoftDeletes, ValidatingTrait; + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ @@ -98,6 +101,14 @@ class Transaction extends Model return $this->belongsToMany('FireflyIII\Models\Category'); } + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function foreignCurrency() + { + return $this->belongsTo('FireflyIII\Models\TransactionCurrency', 'foreign_currency_id'); + } + /** * @param $value * @@ -160,6 +171,14 @@ class Transaction extends Model $this->attributes['amount'] = strval(round($value, 12)); } + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function transactionCurrency() + { + return $this->belongsTo('FireflyIII\Models\TransactionCurrency'); + } + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index 53506bc61d..977ef1ea90 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -19,6 +19,7 @@ use FireflyIII\Support\CacheProperties; use FireflyIII\Support\Models\TransactionJournalTrait; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Log; @@ -66,13 +67,12 @@ class TransactionJournal extends Model /** @var array */ protected $rules = [ - 'user_id' => 'required|exists:users,id', - 'transaction_type_id' => 'required|exists:transaction_types,id', - 'transaction_currency_id' => 'required|exists:transaction_currencies,id', - 'description' => 'required|between:1,1024', - 'completed' => 'required|boolean', - 'date' => 'required|date', - 'encrypted' => 'required|boolean', + 'user_id' => 'required|exists:users,id', + 'transaction_type_id' => 'required|exists:transaction_types,id', + 'description' => 'required|between:1,1024', + 'completed' => 'required|boolean', + 'date' => 'required|date', + 'encrypted' => 'required|boolean', ]; /** @@ -115,7 +115,7 @@ class TransactionJournal extends Model /** * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - public function budgets() + public function budgets(): BelongsToMany { return $this->belongsToMany('FireflyIII\Models\Budget'); } @@ -123,7 +123,7 @@ class TransactionJournal extends Model /** * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - public function categories() + public function categories(): BelongsToMany { return $this->belongsToMany('FireflyIII\Models\Category'); } @@ -204,10 +204,10 @@ class TransactionJournal extends Model /** * @return bool */ - public function isDeposit() + public function isDeposit(): bool { if (!is_null($this->transaction_type_type)) { - return $this->transaction_type_type == TransactionType::DEPOSIT; + return $this->transaction_type_type === TransactionType::DEPOSIT; } return $this->transactionType->isDeposit(); @@ -217,10 +217,10 @@ class TransactionJournal extends Model * * @return bool */ - public function isOpeningBalance() + public function isOpeningBalance(): bool { if (!is_null($this->transaction_type_type)) { - return $this->transaction_type_type == TransactionType::OPENING_BALANCE; + return $this->transaction_type_type === TransactionType::OPENING_BALANCE; } return $this->transactionType->isOpeningBalance(); @@ -230,10 +230,10 @@ class TransactionJournal extends Model * * @return bool */ - public function isTransfer() + public function isTransfer(): bool { if (!is_null($this->transaction_type_type)) { - return $this->transaction_type_type == TransactionType::TRANSFER; + return $this->transaction_type_type === TransactionType::TRANSFER; } return $this->transactionType->isTransfer(); @@ -243,10 +243,10 @@ class TransactionJournal extends Model * * @return bool */ - public function isWithdrawal() + public function isWithdrawal(): bool { if (!is_null($this->transaction_type_type)) { - return $this->transaction_type_type == TransactionType::WITHDRAWAL; + return $this->transaction_type_type === TransactionType::WITHDRAWAL; } return $this->transactionType->isWithdrawal(); @@ -255,7 +255,7 @@ class TransactionJournal extends Model /** * @return \Illuminate\Database\Eloquent\Relations\HasMany */ - public function piggyBankEvents() + public function piggyBankEvents(): HasMany { return $this->hasMany('FireflyIII\Models\PiggyBankEvent'); } @@ -267,7 +267,7 @@ class TransactionJournal extends Model * * @return bool */ - public function save(array $options = []) + public function save(array $options = []): bool { $count = $this->tags()->count(); $this->tag_count = $count; @@ -299,46 +299,6 @@ class TransactionJournal extends Model return $query->where('transaction_journals.date', '<=', $date->format('Y-m-d 00:00:00')); } - /** - * @param EloquentBuilder $query - */ - public function scopeExpanded(EloquentBuilder $query) - { - // left join transaction type: - if (!self::isJoined($query, 'transaction_types')) { - $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); - } - - // left join transaction currency: - $query->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transaction_journals.transaction_currency_id'); - - // extend group by: - $query->groupBy( - [ - 'transaction_journals.id', - 'transaction_journals.created_at', - 'transaction_journals.updated_at', - 'transaction_journals.deleted_at', - 'transaction_journals.user_id', - 'transaction_journals.transaction_type_id', - 'transaction_journals.bill_id', - 'transaction_journals.transaction_currency_id', - 'transaction_journals.description', - 'transaction_journals.date', - 'transaction_journals.interest_date', - 'transaction_journals.book_date', - 'transaction_journals.process_date', - 'transaction_journals.order', - 'transaction_journals.tag_count', - 'transaction_journals.encrypted', - 'transaction_journals.completed', - 'transaction_types.type', - 'transaction_currencies.code', - ] - ); - $query->with(['categories', 'budgets', 'attachments', 'bill', 'transactions']); - } - /** * @param EloquentBuilder $query */ @@ -445,9 +405,9 @@ class TransactionJournal extends Model } /** - * @return \Illuminate\Database\Eloquent\Relations\HasMany + * @return HasMany */ - public function transactions() + public function transactions(): HasMany { return $this->hasMany('FireflyIII\Models\Transaction'); } diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index ef4890bb7f..0e88f0dc8d 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -33,8 +33,6 @@ use FireflyIII\Helpers\Report\PopupReport; use FireflyIII\Helpers\Report\PopupReportInterface; use FireflyIII\Helpers\Report\ReportHelper; use FireflyIII\Helpers\Report\ReportHelperInterface; -use FireflyIII\Import\ImportProcedure; -use FireflyIII\Import\ImportProcedureInterface; use FireflyIII\Repositories\User\UserRepository; use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\Support\Amount; @@ -43,7 +41,7 @@ use FireflyIII\Support\FireflyConfig; use FireflyIII\Support\Navigation; use FireflyIII\Support\Preferences; use FireflyIII\Support\Steam; -use FireflyIII\Support\Twig\Account; +use FireflyIII\Support\Twig\AmountFormat; use FireflyIII\Support\Twig\General; use FireflyIII\Support\Twig\Journal; use FireflyIII\Support\Twig\PiggyBank; @@ -51,11 +49,11 @@ use FireflyIII\Support\Twig\Rule; use FireflyIII\Support\Twig\Transaction; use FireflyIII\Support\Twig\Translation; use FireflyIII\Validation\FireflyValidator; +use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; use Twig; use TwigBridge\Extension\Loader\Functions; use Validator; -use Illuminate\Foundation\Application; /** * Class FireflyServiceProvider @@ -79,7 +77,7 @@ class FireflyServiceProvider extends ServiceProvider Twig::addExtension(new Translation); Twig::addExtension(new Transaction); Twig::addExtension(new Rule); - Twig::addExtension(new Account); + Twig::addExtension(new AmountFormat); } /** @@ -139,7 +137,6 @@ class FireflyServiceProvider extends ServiceProvider // other generators $this->app->bind(ProcessorInterface::class, Processor::class); - $this->app->bind(ImportProcedureInterface::class, ImportProcedure::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); $this->app->bind(AttachmentHelperInterface::class, AttachmentHelper::class); diff --git a/app/Providers/JournalServiceProvider.php b/app/Providers/JournalServiceProvider.php index 42ff742255..d221714c21 100644 --- a/app/Providers/JournalServiceProvider.php +++ b/app/Providers/JournalServiceProvider.php @@ -52,6 +52,9 @@ class JournalServiceProvider extends ServiceProvider $this->registerCollector(); } + /** + * + */ private function registerCollector() { $this->app->bind( @@ -69,6 +72,9 @@ class JournalServiceProvider extends ServiceProvider ); } + /** + * + */ private function registerRepository() { $this->app->bind( @@ -86,6 +92,9 @@ class JournalServiceProvider extends ServiceProvider ); } + /** + * + */ private function registerTasker() { $this->app->bind( @@ -102,4 +111,5 @@ class JournalServiceProvider extends ServiceProvider } ); } + } diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index 423efcd23e..ba8c207939 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -24,9 +24,8 @@ use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\User; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Support\Collection; use Log; +use Validator; /** @@ -37,6 +36,7 @@ use Log; */ class AccountRepository implements AccountRepositoryInterface { + use FindAccountsTrait; /** @var User */ private $user; @@ -77,192 +77,6 @@ class AccountRepository implements AccountRepositoryInterface return true; } - /** - * @param $accountId - * - * @return Account - */ - public function find(int $accountId): Account - { - $account = $this->user->accounts()->find($accountId); - if (is_null($account)) { - return new Account; - } - - return $account; - } - - /** - * @param string $number - * @param array $types - * - * @return Account - */ - public function findByAccountNumber(string $number, array $types): Account - { - $query = $this->user->accounts() - ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id') - ->where('account_meta.name', 'accountNumber') - ->where('account_meta.data', json_encode($number)); - - if (count($types) > 0) { - $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); - $query->whereIn('account_types.type', $types); - } - - /** @var Collection $accounts */ - $accounts = $query->get(['accounts.*']); - if ($accounts->count() > 0) { - return $accounts->first(); - } - - return new Account; - } - - /** - * @param string $iban - * @param array $types - * - * @return Account - */ - public function findByIban(string $iban, array $types): Account - { - $query = $this->user->accounts()->where('iban', '!=', '')->whereNotNull('iban'); - - if (count($types) > 0) { - $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); - $query->whereIn('account_types.type', $types); - } - - $accounts = $query->get(['accounts.*']); - /** @var Account $account */ - foreach ($accounts as $account) { - if ($account->iban === $iban) { - return $account; - } - } - - return new Account; - } - - /** - * @param string $name - * @param array $types - * - * @return Account - */ - public function findByName(string $name, array $types): Account - { - $query = $this->user->accounts(); - - if (count($types) > 0) { - $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); - $query->whereIn('account_types.type', $types); - - } - Log::debug(sprintf('Searching for account named %s of the following type(s)', $name), ['types' => $types]); - - $accounts = $query->get(['accounts.*']); - /** @var Account $account */ - foreach ($accounts as $account) { - if ($account->name === $name) { - Log::debug(sprintf('Found #%d (%s) with type id %d', $account->id, $account->name, $account->account_type_id)); - - return $account; - } - } - Log::debug('Found nothing.'); - - return new Account; - } - - /** - * @param array $accountIds - * - * @return Collection - */ - public function getAccountsById(array $accountIds): Collection - { - /** @var Collection $result */ - $query = $this->user->accounts(); - - if (count($accountIds) > 0) { - $query->whereIn('accounts.id', $accountIds); - } - - $result = $query->get(['accounts.*']); - $result = $result->sortBy( - function (Account $account) { - return strtolower($account->name); - } - ); - - return $result; - } - - /** - * @param array $types - * - * @return Collection - */ - public function getAccountsByType(array $types): Collection - { - /** @var Collection $result */ - $query = $this->user->accounts(); - if (count($types) > 0) { - $query->accountTypeIn($types); - } - - $result = $query->get(['accounts.*']); - $result = $result->sortBy( - function (Account $account) { - return strtolower($account->name); - } - ); - - return $result; - } - - /** - * @param array $types - * - * @return Collection - */ - public function getActiveAccountsByType(array $types): Collection - { - /** @var Collection $result */ - $query = $this->user->accounts()->with( - ['accountmeta' => function (HasMany $query) { - $query->where('name', 'accountRole'); - }] - ); - if (count($types) > 0) { - $query->accountTypeIn($types); - } - $query->where('active', 1); - $result = $query->get(['accounts.*']); - $result = $result->sortBy( - function (Account $account) { - return strtolower($account->name); - } - ); - - return $result; - } - - /** - * @return Account - */ - public function getCashAccount(): Account - { - $type = AccountType::where('type', AccountType::CASH)->first(); - $account = Account::firstOrCreateEncrypted( - ['user_id' => $this->user->id, 'account_type_id' => $type->id, 'name' => 'Cash account', 'active' => 1] - ); - - return $account; - } - /** * Returns the date of the very last transaction in this account. * @@ -365,7 +179,7 @@ class AccountRepository implements AccountRepositoryInterface { // update the account: $account->name = $data['name']; - $account->active = $data['active'] == '1' ? true : false; + $account->active = $data['active'] === '1' ? true : false; $account->virtual_balance = $data['virtualBalance']; $account->iban = $data['iban']; $account->save(); @@ -423,29 +237,32 @@ class AccountRepository implements AccountRepositoryInterface $data['accountType'] = $data['accountType'] ?? 'invalid'; $type = config('firefly.accountTypeByIdentifier.' . $data['accountType']); $accountType = AccountType::whereType($type)->first(); - + $data['iban'] = $this->filterIban($data['iban']); // verify account type if (is_null($accountType)) { throw new FireflyException(sprintf('Account type "%s" is invalid. Cannot create account.', $data['accountType'])); } // account may exist already: - $existingAccount = $this->findByName($data['name'], [$data['accountType']]); + $existingAccount = $this->findByName($data['name'], [$type]); if (!is_null($existingAccount->id)) { - throw new FireflyException(sprintf('There already is an account named "%s" of type "%s".', $data['name'], $data['accountType'])); + Log::warning(sprintf('There already is an account named "%s" of type "%s".', $data['name'], $type)); + + return $existingAccount; } // create it: - $newAccount = new Account( - [ - 'user_id' => $this->user->id, - 'account_type_id' => $accountType->id, - 'name' => $data['name'], - 'virtual_balance' => $data['virtualBalance'], - 'active' => $data['active'] === true ? true : false, - 'iban' => $data['iban'], - ] - ); + $databaseData + = [ + 'user_id' => $this->user->id, + 'account_type_id' => $accountType->id, + 'name' => $data['name'], + 'virtual_balance' => $data['virtualBalance'], + 'active' => $data['active'] === true ? true : false, + 'iban' => $data['iban'], + ]; + $newAccount = new Account($databaseData); + Log::debug('Final account creation dataset', $databaseData); $newAccount->save(); // verify its creation: if (is_null($newAccount->id)) { @@ -453,7 +270,9 @@ class AccountRepository implements AccountRepositoryInterface sprintf('Could not create account "%s" (%d error(s))', $data['name'], $newAccount->getErrors()->count()), $newAccount->getErrors()->toArray() ); throw new FireflyException(sprintf('Tried to create account named "%s" but failed. The logs have more details.', $data['name'])); + } + Log::debug(sprintf('Created new account #%d named "%s" of type %s.', $newAccount->id, $newAccount->name, $accountType->type)); return $newAccount; } @@ -466,7 +285,12 @@ class AccountRepository implements AccountRepositoryInterface */ protected function storeInitialBalance(Account $account, array $data): TransactionJournal { - $amount = $data['openingBalance']; + $amount = strval($data['openingBalance']); + + if (bccomp($amount, '0') === 0) { + return new TransactionJournal; + } + $name = $data['name']; $currencyId = $data['currency_id']; $opposing = $this->storeOpposingAccount($name); @@ -487,18 +311,32 @@ class AccountRepository implements AccountRepositoryInterface $firstAccount = $account; $secondAccount = $opposing; $firstAmount = $amount; - $secondAmount = $amount * -1; + $secondAmount = bcmul($amount, '-1'); if ($data['openingBalance'] < 0) { $firstAccount = $opposing; $secondAccount = $account; - $firstAmount = $amount * -1; + $firstAmount = bcmul($amount, '-1'); $secondAmount = $amount; } - $one = new Transaction(['account_id' => $firstAccount->id, 'transaction_journal_id' => $journal->id, 'amount' => $firstAmount]); + $one = new Transaction( + [ + 'account_id' => $firstAccount->id, + 'transaction_journal_id' => $journal->id, + 'amount' => $firstAmount, + 'transaction_currency_id' => $currencyId, + ] + ); $one->save();// first transaction: from - $two = new Transaction(['account_id' => $secondAccount->id, 'transaction_journal_id' => $journal->id, 'amount' => $secondAmount]); + + $two = new Transaction( + [ + 'account_id' => $secondAccount->id, + 'transaction_journal_id' => $journal->id, + 'amount' => $secondAmount, + 'transaction_currency_id' => $currencyId,] + ); $two->save(); // second transaction: to Log::debug(sprintf('Stored two transactions, #%d and #%d', $one->id, $two->id)); @@ -606,9 +444,15 @@ class AccountRepository implements AccountRepositoryInterface protected function updateOpeningBalanceJournal(Account $account, TransactionJournal $journal, array $data): bool { $date = $data['openingBalanceDate']; - $amount = $data['openingBalance']; + $amount = strval($data['openingBalance']); $currencyId = intval($data['currency_id']); + if (bccomp($amount, '0') === 0) { + $journal->delete(); + + return true; + } + // update date: $journal->date = $date; $journal->transaction_currency_id = $currencyId; @@ -616,12 +460,14 @@ class AccountRepository implements AccountRepositoryInterface // update transactions: /** @var Transaction $transaction */ foreach ($journal->transactions()->get() as $transaction) { - if ($account->id == $transaction->account_id) { - $transaction->amount = $amount; + if ($account->id === $transaction->account_id) { + $transaction->amount = $amount; + $transaction->transaction_currency_id = $currencyId; $transaction->save(); } - if ($account->id != $transaction->account_id) { - $transaction->amount = $amount * -1; + if ($account->id !== $transaction->account_id) { + $transaction->amount = bcmul($amount, '-1'); + $transaction->transaction_currency_id = $currencyId; $transaction->save(); } } @@ -631,6 +477,7 @@ class AccountRepository implements AccountRepositoryInterface } + /** * @param array $data * @@ -638,9 +485,7 @@ class AccountRepository implements AccountRepositoryInterface */ protected function validOpeningBalanceData(array $data): bool { - if (isset($data['openingBalance']) && isset($data['openingBalanceDate']) - && bccomp(strval($data['openingBalance']), '0') !== 0 - ) { + if (isset($data['openingBalance']) && isset($data['openingBalanceDate'])) { Log::debug('Array has valid opening balance data.'); return true; @@ -649,4 +494,26 @@ class AccountRepository implements AccountRepositoryInterface return false; } + + /** + * @param string $iban + * + * @return null|string + */ + private function filterIban(string $iban = null) + { + if (is_null($iban)) { + return null; + } + $data = ['iban' => $iban]; + $rules = ['iban' => 'required|iban']; + $validator = Validator::make($data, $rules); + if ($validator->fails()) { + Log::error(sprintf('Detected invalid IBAN ("%s"). Return NULL instead.', $iban)); + + return null; + } + + return $iban; + } } diff --git a/app/Repositories/Account/AccountRepositoryInterface.php b/app/Repositories/Account/AccountRepositoryInterface.php index e444fcada0..a9e04b03d6 100644 --- a/app/Repositories/Account/AccountRepositoryInterface.php +++ b/app/Repositories/Account/AccountRepositoryInterface.php @@ -36,11 +36,6 @@ interface AccountRepositoryInterface */ public function count(array $types): int; - /** - * @return Account - */ - public function getCashAccount(): Account; - /** * Moved here from account CRUD. * @@ -103,6 +98,11 @@ interface AccountRepositoryInterface */ public function getActiveAccountsByType(array $types): Collection; + /** + * @return Account + */ + public function getCashAccount(): Account; + /** * Returns the date of the very last transaction in this account. * diff --git a/app/Repositories/Account/AccountTasker.php b/app/Repositories/Account/AccountTasker.php index ce2e437880..3759522c41 100644 --- a/app/Repositories/Account/AccountTasker.php +++ b/app/Repositories/Account/AccountTasker.php @@ -41,11 +41,10 @@ class AccountTasker implements AccountTaskerInterface */ public function getAccountReport(Collection $accounts, Carbon $start, Carbon $end): array { - $ids = $accounts->pluck('id')->toArray(); $yesterday = clone $start; $yesterday->subDay(); - $startSet = Steam::balancesById($ids, $yesterday); - $endSet = Steam::balancesById($ids, $end); + $startSet = Steam::balancesByAccounts($accounts, $yesterday); + $endSet = Steam::balancesByAccounts($accounts, $end); Log::debug('Start of accountreport'); diff --git a/app/Repositories/Account/FindAccountsTrait.php b/app/Repositories/Account/FindAccountsTrait.php new file mode 100644 index 0000000000..e06a598a47 --- /dev/null +++ b/app/Repositories/Account/FindAccountsTrait.php @@ -0,0 +1,215 @@ +user->accounts()->find($accountId); + if (is_null($account)) { + return new Account; + } + + return $account; + } + + /** + * @param string $number + * @param array $types + * + * @return Account + */ + public function findByAccountNumber(string $number, array $types): Account + { + $query = $this->user->accounts() + ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id') + ->where('account_meta.name', 'accountNumber') + ->where('account_meta.data', json_encode($number)); + + if (count($types) > 0) { + $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); + $query->whereIn('account_types.type', $types); + } + + /** @var Collection $accounts */ + $accounts = $query->get(['accounts.*']); + if ($accounts->count() > 0) { + return $accounts->first(); + } + + return new Account; + } + + /** + * @param string $iban + * @param array $types + * + * @return Account + */ + public function findByIban(string $iban, array $types): Account + { + $query = $this->user->accounts()->where('iban', '!=', '')->whereNotNull('iban'); + + if (count($types) > 0) { + $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); + $query->whereIn('account_types.type', $types); + } + + $accounts = $query->get(['accounts.*']); + /** @var Account $account */ + foreach ($accounts as $account) { + if ($account->iban === $iban) { + return $account; + } + } + + return new Account; + } + + /** + * @param string $name + * @param array $types + * + * @return Account + */ + public function findByName(string $name, array $types): Account + { + $query = $this->user->accounts(); + + if (count($types) > 0) { + $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); + $query->whereIn('account_types.type', $types); + + } + Log::debug(sprintf('Searching for account named "%s" (of user #%d) of the following type(s)', $name, $this->user->id), ['types' => $types]); + + $accounts = $query->get(['accounts.*']); + /** @var Account $account */ + foreach ($accounts as $account) { + if ($account->name === $name) { + Log::debug(sprintf('Found #%d (%s) with type id %d', $account->id, $account->name, $account->account_type_id)); + + return $account; + } + } + Log::debug(sprintf('There is no account with name "%s" or types', $name), $types); + + return new Account; + } + + /** + * @param array $accountIds + * + * @return Collection + */ + public function getAccountsById(array $accountIds): Collection + { + /** @var Collection $result */ + $query = $this->user->accounts(); + + if (count($accountIds) > 0) { + $query->whereIn('accounts.id', $accountIds); + } + + $result = $query->get(['accounts.*']); + $result = $result->sortBy( + function (Account $account) { + return strtolower($account->name); + } + ); + + return $result; + } + + /** + * @param array $types + * + * @return Collection + */ + public function getAccountsByType(array $types): Collection + { + /** @var Collection $result */ + $query = $this->user->accounts(); + if (count($types) > 0) { + $query->accountTypeIn($types); + } + + $result = $query->get(['accounts.*']); + $result = $result->sortBy( + function (Account $account) { + return strtolower($account->name); + } + ); + + return $result; + } + + /** + * @param array $types + * + * @return Collection + */ + public function getActiveAccountsByType(array $types): Collection + { + /** @var Collection $result */ + $query = $this->user->accounts()->with( + ['accountmeta' => function (HasMany $query) { + $query->where('name', 'accountRole'); + }] + ); + if (count($types) > 0) { + $query->accountTypeIn($types); + } + $query->where('active', 1); + $result = $query->get(['accounts.*']); + $result = $result->sortBy( + function (Account $account) { + return strtolower($account->name); + } + ); + + return $result; + } + + /** + * @return Account + */ + public function getCashAccount(): Account + { + $type = AccountType::where('type', AccountType::CASH)->first(); + $account = Account::firstOrCreateEncrypted( + ['user_id' => $this->user->id, 'account_type_id' => $type->id, 'name' => 'Cash account', 'active' => 1] + ); + + return $account; + } +} diff --git a/app/Repositories/Attachment/AttachmentRepository.php b/app/Repositories/Attachment/AttachmentRepository.php index b7c174b103..91a3dcbd73 100644 --- a/app/Repositories/Attachment/AttachmentRepository.php +++ b/app/Repositories/Attachment/AttachmentRepository.php @@ -19,6 +19,7 @@ use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; use FireflyIII\Models\Attachment; use FireflyIII\User; use Illuminate\Support\Collection; +use Log; use Storage; /** @@ -95,16 +96,20 @@ class AttachmentRepository implements AttachmentRepositoryInterface public function getContent(Attachment $attachment): string { // create a disk. - $disk = Storage::disk('upload'); - $file = $attachment->fileName(); + $disk = Storage::disk('upload'); + $file = $attachment->fileName(); + $content = ''; if ($disk->exists($file)) { $content = Crypt::decrypt($disk->get($file)); + } + if (is_bool($content)) { + Log::error(sprintf('Attachment #%d may be corrupted: the content could not be decrypted.', $attachment->id)); - return $content; + return ''; } - return ''; + return $content; } /** diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index 43599dc820..e6957abe4b 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -116,7 +116,7 @@ class BillRepository implements BillRepositoryInterface $set = $set->sortBy( function (Bill $bill) { - $int = $bill->active == 1 ? 0 : 1; + $int = $bill->active === 1 ? 0 : 1; return $int . strtolower($bill->name); } @@ -168,7 +168,7 @@ class BillRepository implements BillRepositoryInterface $set = $set->sortBy( function (Bill $bill) { - $int = $bill->active == 1 ? 0 : 1; + $int = $bill->active === 1 ? 0 : 1; return $int . strtolower($bill->name); } @@ -262,6 +262,8 @@ class BillRepository implements BillRepositoryInterface } /** + * The "paid dates" list is a list of dates of transaction journals that are linked to this bill. + * * @param Bill $bill * @param Carbon $start * @param Carbon $end @@ -498,7 +500,7 @@ class BillRepository implements BillRepositoryInterface return true; } - if ($bill->id == $journal->bill_id) { + if ($bill->id === $journal->bill_id) { // if no match, but bill used to match, remove it: $journal->bill_id = null; $journal->save(); diff --git a/app/Repositories/Category/CategoryRepository.php b/app/Repositories/Category/CategoryRepository.php index bef618f634..e0a9f262b7 100644 --- a/app/Repositories/Category/CategoryRepository.php +++ b/app/Repositories/Category/CategoryRepository.php @@ -160,42 +160,19 @@ class CategoryRepository implements CategoryRepositoryInterface */ public function lastUseDate(Category $category, Collection $accounts): Carbon { - $last = null; + $last = new Carbon('1900-01-01'); + $lastJournalDate = $this->getLastJournalDate($category, $accounts); - /** @var TransactionJournal $first */ - $lastJournalQuery = $category->transactionJournals()->orderBy('date', 'DESC'); - - if ($accounts->count() > 0) { - // filter journals: - $ids = $accounts->pluck('id')->toArray(); - $lastJournalQuery->leftJoin('transactions as t', 't.transaction_journal_id', '=', 'transaction_journals.id'); - $lastJournalQuery->whereIn('t.account_id', $ids); + if ($lastJournalDate->year !== 1900) { + $last = clone $lastJournalDate; + unset($lastJournalDate); } - $lastJournal = $lastJournalQuery->first(['transaction_journals.*']); + $lastTransactionDate = $this->getLastTransactionDate($category, $accounts); - if ($lastJournal) { - $last = $lastJournal->date; - } - - // check transactions: - - $lastTransactionQuery = $category->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->orderBy('transaction_journals.date', 'DESC'); - if ($accounts->count() > 0) { - // filter journals: - $ids = $accounts->pluck('id')->toArray(); - $lastTransactionQuery->whereIn('transactions.account_id', $ids); - } - - $lastTransaction = $lastTransactionQuery->first(['transaction_journals.*']); - if (!is_null($lastTransaction) && ((!is_null($last) && $lastTransaction->date < $last) || is_null($last))) { - $last = new Carbon($lastTransaction->date); - } - - if (is_null($last)) { - return new Carbon('1900-01-01'); + if ($lastTransactionDate->year !== 1900 && $lastTransactionDate < $last) { + $last = clone $lastTransactionDate; + unset($lastTransactionDate); } return $last; @@ -479,4 +456,53 @@ class CategoryRepository implements CategoryRepositoryInterface return $category; } + /** + * @param Category $category + * @param Collection $accounts + * + * @return Carbon + */ + private function getLastJournalDate(Category $category, Collection $accounts): Carbon + { + $query = $category->transactionJournals()->orderBy('date', 'DESC'); + + if ($accounts->count() > 0) { + $query->leftJoin('transactions as t', 't.transaction_journal_id', '=', 'transaction_journals.id'); + $query->whereIn('t.account_id', $accounts->pluck('id')->toArray()); + } + + $result = $query->first(['transaction_journals.*']); + + if (!is_null($result)) { + return $result->date; + } + + return new Carbon('1900-01-01'); + } + + /** + * @param Category $category + * @param Collection $accounts + * + * @return Carbon + */ + private function getLastTransactionDate(Category $category, Collection $accounts): Carbon + { + // check transactions: + $query = $category->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->orderBy('transaction_journals.date', 'DESC'); + if ($accounts->count() > 0) { + // filter journals: + $query->whereIn('transactions.account_id', $accounts->pluck('id')->toArray()); + } + + $lastTransaction = $query->first(['transaction_journals.*']); + if (!is_null($lastTransaction)) { + return new Carbon($lastTransaction->date); + } + + return new Carbon('1900-01-01'); + } + } diff --git a/app/Repositories/ImportJob/ImportJobRepository.php b/app/Repositories/ImportJob/ImportJobRepository.php index 6af984a06c..36bd597406 100644 --- a/app/Repositories/ImportJob/ImportJobRepository.php +++ b/app/Repositories/ImportJob/ImportJobRepository.php @@ -13,10 +13,16 @@ declare(strict_types=1); namespace FireflyIII\Repositories\ImportJob; +use Crypt; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Support\Str; +use Log; +use SplFileObject; +use Storage; +use Symfony\Component\HttpFoundation\File\UploadedFile; /** * Class ImportJobRepository @@ -51,13 +57,13 @@ class ImportJobRepository implements ImportJobRepositoryInterface $importJob->user()->associate($this->user); $importJob->file_type = $fileType; $importJob->key = Str::random(12); - $importJob->status = 'import_status_never_started'; + $importJob->status = 'new'; + $importJob->configuration = []; $importJob->extended_status = [ - 'total_steps' => 0, - 'steps_done' => 0, - 'import_count' => 0, - 'importTag' => 0, - 'errors' => [], + 'steps' => 0, + 'done' => 0, + 'tag' => 0, + 'errors' => [], ]; $importJob->save(); @@ -86,6 +92,75 @@ class ImportJobRepository implements ImportJobRepositoryInterface return $result; } + /** + * @param ImportJob $job + * @param UploadedFile $file + * + * @return bool + */ + public function processConfiguration(ImportJob $job, UploadedFile $file): bool + { + /** @var UserRepositoryInterface $repository */ + $repository = app(UserRepositoryInterface::class); + // demo user's configuration upload is ignored completely. + if (!$repository->hasRole($this->user, 'demo')) { + Log::debug( + 'Uploaded configuration file', ['name' => $file->getClientOriginalName(), 'size' => $file->getSize(), 'mime' => $file->getClientMimeType()] + ); + + $configFileObject = new SplFileObject($file->getRealPath()); + $configRaw = $configFileObject->fread($configFileObject->getSize()); + $configuration = json_decode($configRaw, true); + + if (!is_null($configuration) && is_array($configuration)) { + Log::debug('Found configuration', $configuration); + $this->setConfiguration($job, $configuration); + } + } + + return true; + } + + /** + * @param ImportJob $job + * @param UploadedFile $file + * + * @return mixed + */ + public function processFile(ImportJob $job, UploadedFile $file): bool + { + /** @var UserRepositoryInterface $repository */ + $repository = app(UserRepositoryInterface::class); + $newName = sprintf('%s.upload', $job->key); + $uploaded = new SplFileObject($file->getRealPath()); + $content = $uploaded->fread($uploaded->getSize()); + $contentEncrypted = Crypt::encrypt($content); + $disk = Storage::disk('upload'); + + + // user is demo user, replace upload with prepared file. + if ($repository->hasRole($this->user, 'demo')) { + $stubsDisk = Storage::disk('stubs'); + $content = $stubsDisk->get('demo-import.csv'); + $contentEncrypted = Crypt::encrypt($content); + $disk->put($newName, $contentEncrypted); + Log::debug('Replaced upload with demo file.'); + + // also set up prepared configuration. + $configuration = json_decode($stubsDisk->get('demo-configuration.json'), true); + $this->setConfiguration($job, $configuration); + Log::debug('Set configuration for demo user', $configuration); + } + + if (!$repository->hasRole($this->user, 'demo')) { + // user is not demo, process original upload: + $disk->put($newName, $contentEncrypted); + Log::debug('Uploaded file', ['name' => $file->getClientOriginalName(), 'size' => $file->getSize(), 'mime' => $file->getClientMimeType()]); + } + + return true; + } + /** * @param ImportJob $job * @param array $configuration diff --git a/app/Repositories/ImportJob/ImportJobRepositoryInterface.php b/app/Repositories/ImportJob/ImportJobRepositoryInterface.php index 5bdf636d42..bfdc0493b9 100644 --- a/app/Repositories/ImportJob/ImportJobRepositoryInterface.php +++ b/app/Repositories/ImportJob/ImportJobRepositoryInterface.php @@ -15,6 +15,7 @@ namespace FireflyIII\Repositories\ImportJob; use FireflyIII\Models\ImportJob; use FireflyIII\User; +use Symfony\Component\HttpFoundation\File\UploadedFile; /** * Interface ImportJobRepositoryInterface @@ -37,6 +38,22 @@ interface ImportJobRepositoryInterface */ public function findByKey(string $key): ImportJob; + /** + * @param ImportJob $job + * @param UploadedFile $file + * + * @return bool + */ + public function processConfiguration(ImportJob $job, UploadedFile $file): bool; + + /** + * @param ImportJob $job + * @param UploadedFile $file + * + * @return mixed + */ + public function processFile(ImportJob $job, UploadedFile $file): bool; + /** * @param ImportJob $job * @param array $configuration diff --git a/app/Repositories/Journal/CreateJournalsTrait.php b/app/Repositories/Journal/CreateJournalsTrait.php new file mode 100644 index 0000000000..44064b7432 --- /dev/null +++ b/app/Repositories/Journal/CreateJournalsTrait.php @@ -0,0 +1,191 @@ + 0) { + $tag = Tag::firstOrCreateEncrypted(['tag' => $name, 'user_id' => $journal->user_id]); + if (!is_null($tag)) { + Log::debug(sprintf('Will try to connect tag #%d to journal #%d.', $tag->id, $journal->id)); + $tagRepository->connect($journal, $tag); + } + } + } + + return true; + } + + /** + * @param Transaction $transaction + * @param int $budgetId + */ + protected function storeBudgetWithTransaction(Transaction $transaction, int $budgetId) + { + if (intval($budgetId) > 0 && $transaction->transactionJournal->transactionType->type !== TransactionType::TRANSFER) { + /** @var \FireflyIII\Models\Budget $budget */ + $budget = Budget::find($budgetId); + $transaction->budgets()->save($budget); + } + } + + /** + * @param Transaction $transaction + * @param string $category + */ + protected function storeCategoryWithTransaction(Transaction $transaction, string $category) + { + if (strlen($category) > 0) { + $category = Category::firstOrCreateEncrypted(['name' => $category, 'user_id' => $transaction->transactionJournal->user_id]); + $transaction->categories()->save($category); + } + } + + /** + * The reference to storeAccounts() in this function is an indication of spagetti code but alas, + * I leave it as it is. + * + * @param TransactionJournal $journal + * @param array $transaction + * @param int $identifier + * + * @return Collection + */ + protected function storeSplitTransaction(TransactionJournal $journal, array $transaction, int $identifier): Collection + { + // store source and destination accounts (depends on type) + $accounts = $this->storeAccounts($this->user, $journal->transactionType, $transaction); + + // store transaction one way: + $amount = bcmul(strval($transaction['amount']), '-1'); + $foreignAmount = is_null($transaction['foreign_amount']) ? null : bcmul(strval($transaction['foreign_amount']), '-1'); + $one = $this->storeTransaction( + [ + 'journal' => $journal, + 'account' => $accounts['source'], + 'amount' => $amount, + 'transaction_currency_id' => $transaction['transaction_currency_id'], + 'foreign_amount' => $foreignAmount, + 'foreign_currency_id' => $transaction['foreign_currency_id'], + 'description' => $transaction['description'], + 'category' => null, + 'budget' => null, + 'identifier' => $identifier, + ] + ); + $this->storeCategoryWithTransaction($one, $transaction['category']); + $this->storeBudgetWithTransaction($one, $transaction['budget_id']); + + // and the other way: + $amount = strval($transaction['amount']); + $foreignAmount = is_null($transaction['foreign_amount']) ? null : strval($transaction['foreign_amount']); + $two = $this->storeTransaction( + [ + 'journal' => $journal, + 'account' => $accounts['destination'], + 'amount' => $amount, + 'transaction_currency_id' => $transaction['transaction_currency_id'], + 'foreign_amount' => $foreignAmount, + 'foreign_currency_id' => $transaction['foreign_currency_id'], + 'description' => $transaction['description'], + 'category' => null, + 'budget' => null, + 'identifier' => $identifier, + ] + ); + $this->storeCategoryWithTransaction($two, $transaction['category']); + $this->storeBudgetWithTransaction($two, $transaction['budget_id']); + + return new Collection([$one, $two]); + } + + /** + * @param array $data + * + * @return Transaction + */ + protected function storeTransaction(array $data): Transaction + { + $fields = [ + 'transaction_journal_id' => $data['journal']->id, + 'account_id' => $data['account']->id, + 'amount' => $data['amount'], + 'foreign_amount' => $data['foreign_amount'], + 'transaction_currency_id' => $data['transaction_currency_id'], + 'foreign_currency_id' => $data['foreign_currency_id'], + 'description' => $data['description'], + 'identifier' => $data['identifier'], + ]; + + + if (is_null($data['foreign_currency_id'])) { + unset($fields['foreign_currency_id']); + } + if (is_null($data['foreign_amount'])) { + unset($fields['foreign_amount']); + } + + /** @var Transaction $transaction */ + $transaction = Transaction::create($fields); + + Log::debug(sprintf('Transaction stored with ID: %s', $transaction->id)); + + if (!is_null($data['category'])) { + $transaction->categories()->save($data['category']); + } + + if (!is_null($data['budget'])) { + $transaction->categories()->save($data['budget']); + } + + return $transaction; + + } + +} diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 98ab2dafb5..774ae77947 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -13,17 +13,9 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Journal; -use DB; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; -use FireflyIII\Models\Budget; -use FireflyIII\Models\Category; -use FireflyIII\Models\Tag; -use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; -use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\User; use Illuminate\Support\Collection; use Illuminate\Support\MessageBag; @@ -37,14 +29,12 @@ use Preferences; */ class JournalRepository implements JournalRepositoryInterface { + use CreateJournalsTrait, UpdateJournalsTrait, SupportJournalsTrait; + /** @var User */ private $user; - /** @var array */ - private $validMetaFields - = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date', 'internal_reference', 'notes', 'foreign_amount', - 'foreign_currency_id', - ]; + private $validMetaFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date', 'internal_reference', 'notes']; /** * @param TransactionJournal $journal @@ -180,15 +170,14 @@ class JournalRepository implements JournalRepositoryInterface // find transaction type. /** @var TransactionType $transactionType */ $transactionType = TransactionType::where('type', ucfirst($data['what']))->first(); - $accounts = $this->storeAccounts($transactionType, $data); + $accounts = $this->storeAccounts($this->user, $transactionType, $data); $data = $this->verifyNativeAmount($data, $accounts); - $currencyId = $data['currency_id']; $amount = strval($data['amount']); $journal = new TransactionJournal( [ 'user_id' => $this->user->id, 'transaction_type_id' => $transactionType->id, - 'transaction_currency_id' => $currencyId, + 'transaction_currency_id' => $data['currency_id'], // no longer used. 'description' => $data['description'], 'completed' => 0, 'date' => $data['date'], @@ -200,27 +189,32 @@ class JournalRepository implements JournalRepositoryInterface $this->storeCategoryWithJournal($journal, $data['category']); $this->storeBudgetWithJournal($journal, $data['budget_id']); - // store two transactions: $one = [ - 'journal' => $journal, - 'account' => $accounts['source'], - 'amount' => bcmul($amount, '-1'), - 'description' => null, - 'category' => null, - 'budget' => null, - 'identifier' => 0, + 'journal' => $journal, + 'account' => $accounts['source'], + 'amount' => bcmul($amount, '-1'), + 'transaction_currency_id' => $data['currency_id'], + 'foreign_amount' => is_null($data['foreign_amount']) ? null : bcmul(strval($data['foreign_amount']), '-1'), + 'foreign_currency_id' => $data['foreign_currency_id'], + 'description' => null, + 'category' => null, + 'budget' => null, + 'identifier' => 0, ]; $this->storeTransaction($one); $two = [ - 'journal' => $journal, - 'account' => $accounts['destination'], - 'amount' => $amount, - 'description' => null, - 'category' => null, - 'budget' => null, - 'identifier' => 0, + 'journal' => $journal, + 'account' => $accounts['destination'], + 'amount' => $amount, + 'transaction_currency_id' => $data['currency_id'], + 'foreign_amount' => $data['foreign_amount'], + 'foreign_currency_id' => $data['foreign_currency_id'], + 'description' => null, + 'category' => null, + 'budget' => null, + 'identifier' => 0, ]; $this->storeTransaction($two); @@ -256,20 +250,12 @@ class JournalRepository implements JournalRepositoryInterface { // update actual journal: - $journal->description = $data['description']; - $journal->date = $data['date']; - $accounts = $this->storeAccounts($journal->transactionType, $data); - $amount = strval($data['amount']); - - if ($data['currency_id'] !== $journal->transaction_currency_id) { - // user has entered amount in foreign currency. - // amount in "our" currency is $data['exchanged_amount']: - $amount = strval($data['exchanged_amount']); - // other values must be stored as well: - $data['original_amount'] = $data['amount']; - $data['original_currency_id'] = $data['currency_id']; - - } + $journal->description = $data['description']; + $journal->date = $data['date']; + $accounts = $this->storeAccounts($this->user, $journal->transactionType, $data); + $data = $this->verifyNativeAmount($data, $accounts); + $data['amount'] = strval($data['amount']); + $data['foreign_amount'] = is_null($data['foreign_amount']) ? null : strval($data['foreign_amount']); // unlink all categories, recreate them: $journal->categories()->detach(); @@ -278,9 +264,11 @@ class JournalRepository implements JournalRepositoryInterface $this->storeCategoryWithJournal($journal, $data['category']); $this->storeBudgetWithJournal($journal, $data['budget_id']); + // negative because source loses money. + $this->updateSourceTransaction($journal, $accounts['source'], $data); - $this->updateSourceTransaction($journal, $accounts['source'], bcmul($amount, '-1')); // negative because source loses money. - $this->updateDestinationTransaction($journal, $accounts['destination'], $amount); // positive because destination gets money. + // positive because destination gets money. + $this->updateDestinationTransaction($journal, $accounts['destination'], $data); $journal->save(); @@ -317,9 +305,8 @@ class JournalRepository implements JournalRepositoryInterface public function updateSplitJournal(TransactionJournal $journal, array $data): TransactionJournal { // update actual journal: - $journal->transaction_currency_id = $data['currency_id']; - $journal->description = $data['journal_description']; - $journal->date = $data['date']; + $journal->description = $data['journal_description']; + $journal->date = $data['date']; $journal->save(); Log::debug(sprintf('Updated split journal #%d', $journal->id)); @@ -339,7 +326,6 @@ class JournalRepository implements JournalRepositoryInterface } } - // update tags: if (isset($data['tags']) && is_array($data['tags'])) { $this->updateTags($journal, $data['tags']); @@ -351,6 +337,7 @@ class JournalRepository implements JournalRepositoryInterface // store each transaction. $identifier = 0; Log::debug(sprintf('Count %d transactions in updateSplitJournal()', count($data['transactions']))); + foreach ($data['transactions'] as $transaction) { Log::debug(sprintf('Split journal update split transaction %d', $identifier)); $transaction = $this->appendTransactionData($transaction, $data); @@ -362,466 +349,4 @@ class JournalRepository implements JournalRepositoryInterface return $journal; } - - /** - * When the user edits a split journal, each line is missing crucial data: - * - * - Withdrawal lines are missing the source account ID - * - Deposit lines are missing the destination account ID - * - Transfers are missing both. - * - * We need to append the array. - * - * @param array $transaction - * @param array $data - * - * @return array - */ - private function appendTransactionData(array $transaction, array $data): array - { - switch ($data['what']) { - case strtolower(TransactionType::TRANSFER): - case strtolower(TransactionType::WITHDRAWAL): - $transaction['source_account_id'] = intval($data['journal_source_account_id']); - break; - } - - switch ($data['what']) { - case strtolower(TransactionType::TRANSFER): - case strtolower(TransactionType::DEPOSIT): - $transaction['destination_account_id'] = intval($data['journal_destination_account_id']); - break; - } - - return $transaction; - } - - /** - * - * * Remember: a balancingAct takes at most one expense and one transfer. - * an advancePayment takes at most one expense, infinite deposits and NO transfers. - * - * @param TransactionJournal $journal - * @param array $array - * - * @return bool - */ - private function saveTags(TransactionJournal $journal, array $array): bool - { - /** @var TagRepositoryInterface $tagRepository */ - $tagRepository = app(TagRepositoryInterface::class); - - foreach ($array as $name) { - if (strlen(trim($name)) > 0) { - $tag = Tag::firstOrCreateEncrypted(['tag' => $name, 'user_id' => $journal->user_id]); - if (!is_null($tag)) { - Log::debug(sprintf('Will try to connect tag #%d to journal #%d.', $tag->id, $journal->id)); - $tagRepository->connect($journal, $tag); - } - } - } - - return true; - } - - /** - * @param TransactionType $type - * @param array $data - * - * @return array - * @throws FireflyException - */ - private function storeAccounts(TransactionType $type, array $data): array - { - $accounts = [ - 'source' => null, - 'destination' => null, - ]; - - Log::debug(sprintf('Going to store accounts for type %s', $type->type)); - switch ($type->type) { - case TransactionType::WITHDRAWAL: - $accounts = $this->storeWithdrawalAccounts($data); - break; - - case TransactionType::DEPOSIT: - $accounts = $this->storeDepositAccounts($data); - - break; - case TransactionType::TRANSFER: - $accounts['source'] = Account::where('user_id', $this->user->id)->where('id', $data['source_account_id'])->first(); - $accounts['destination'] = Account::where('user_id', $this->user->id)->where('id', $data['destination_account_id'])->first(); - break; - default: - throw new FireflyException(sprintf('Did not recognise transaction type "%s".', $type->type)); - } - - if (is_null($accounts['source'])) { - Log::error('"source"-account is null, so we cannot continue!', ['data' => $data]); - throw new FireflyException('"source"-account is null, so we cannot continue!'); - } - - if (is_null($accounts['destination'])) { - Log::error('"destination"-account is null, so we cannot continue!', ['data' => $data]); - throw new FireflyException('"destination"-account is null, so we cannot continue!'); - - } - - - return $accounts; - } - - /** - * @param TransactionJournal $journal - * @param int $budgetId - */ - private function storeBudgetWithJournal(TransactionJournal $journal, int $budgetId) - { - if (intval($budgetId) > 0 && $journal->transactionType->type === TransactionType::WITHDRAWAL) { - /** @var \FireflyIII\Models\Budget $budget */ - $budget = Budget::find($budgetId); - $journal->budgets()->save($budget); - } - } - - /** - * @param Transaction $transaction - * @param int $budgetId - */ - private function storeBudgetWithTransaction(Transaction $transaction, int $budgetId) - { - if (intval($budgetId) > 0 && $transaction->transactionJournal->transactionType->type !== TransactionType::TRANSFER) { - /** @var \FireflyIII\Models\Budget $budget */ - $budget = Budget::find($budgetId); - $transaction->budgets()->save($budget); - } - } - - /** - * @param TransactionJournal $journal - * @param string $category - */ - private function storeCategoryWithJournal(TransactionJournal $journal, string $category) - { - if (strlen($category) > 0) { - $category = Category::firstOrCreateEncrypted(['name' => $category, 'user_id' => $journal->user_id]); - $journal->categories()->save($category); - } - } - - /** - * @param Transaction $transaction - * @param string $category - */ - private function storeCategoryWithTransaction(Transaction $transaction, string $category) - { - if (strlen($category) > 0) { - $category = Category::firstOrCreateEncrypted(['name' => $category, 'user_id' => $transaction->transactionJournal->user_id]); - $transaction->categories()->save($category); - } - } - - /** - * @param array $data - * - * @return array - */ - private function storeDepositAccounts(array $data): array - { - Log::debug('Now in storeDepositAccounts().'); - $destinationAccount = Account::where('user_id', $this->user->id)->where('id', $data['destination_account_id'])->first(['accounts.*']); - - Log::debug(sprintf('Destination account is #%d ("%s")', $destinationAccount->id, $destinationAccount->name)); - - if (strlen($data['source_account_name']) > 0) { - $sourceType = AccountType::where('type', 'Revenue account')->first(); - $sourceAccount = Account::firstOrCreateEncrypted( - ['user_id' => $this->user->id, 'account_type_id' => $sourceType->id, 'name' => $data['source_account_name'], 'active' => 1] - ); - - Log::debug(sprintf('source account name is "%s", account is %d', $data['source_account_name'], $sourceAccount->id)); - - return [ - 'source' => $sourceAccount, - 'destination' => $destinationAccount, - ]; - } - - Log::debug('source_account_name is empty, so default to cash account!'); - - $sourceType = AccountType::where('type', AccountType::CASH)->first(); - $sourceAccount = Account::firstOrCreateEncrypted( - ['user_id' => $this->user->id, 'account_type_id' => $sourceType->id, 'name' => 'Cash account', 'active' => 1] - ); - - return [ - 'source' => $sourceAccount, - 'destination' => $destinationAccount, - ]; - } - - /** - * @param TransactionJournal $journal - * @param array $transaction - * @param int $identifier - * - * @return Collection - */ - private function storeSplitTransaction(TransactionJournal $journal, array $transaction, int $identifier): Collection - { - // store source and destination accounts (depends on type) - $accounts = $this->storeAccounts($journal->transactionType, $transaction); - - // store transaction one way: - $one = $this->storeTransaction( - [ - 'journal' => $journal, - 'account' => $accounts['source'], - 'amount' => bcmul(strval($transaction['amount']), '-1'), - 'description' => $transaction['description'], - 'category' => null, - 'budget' => null, - 'identifier' => $identifier, - ] - ); - $this->storeCategoryWithTransaction($one, $transaction['category']); - $this->storeBudgetWithTransaction($one, $transaction['budget_id']); - - // and the other way: - $two = $this->storeTransaction( - [ - 'journal' => $journal, - 'account' => $accounts['destination'], - 'amount' => strval($transaction['amount']), - 'description' => $transaction['description'], - 'category' => null, - 'budget' => null, - 'identifier' => $identifier, - ] - ); - $this->storeCategoryWithTransaction($two, $transaction['category']); - $this->storeBudgetWithTransaction($two, $transaction['budget_id']); - - return new Collection([$one, $two]); - } - - /** - * @param array $data - * - * @return Transaction - */ - private function storeTransaction(array $data): Transaction - { - /** @var Transaction $transaction */ - $transaction = Transaction::create( - [ - 'transaction_journal_id' => $data['journal']->id, - 'account_id' => $data['account']->id, - 'amount' => $data['amount'], - 'description' => $data['description'], - 'identifier' => $data['identifier'], - ] - ); - - Log::debug(sprintf('Transaction stored with ID: %s', $transaction->id)); - - if (!is_null($data['category'])) { - $transaction->categories()->save($data['category']); - } - - if (!is_null($data['budget'])) { - $transaction->categories()->save($data['budget']); - } - - return $transaction; - - } - - /** - * @param array $data - * - * @return array - */ - private function storeWithdrawalAccounts(array $data): array - { - Log::debug('Now in storeWithdrawalAccounts().'); - $sourceAccount = Account::where('user_id', $this->user->id)->where('id', $data['source_account_id'])->first(['accounts.*']); - - Log::debug(sprintf('Source account is #%d ("%s")', $sourceAccount->id, $sourceAccount->name)); - - if (strlen($data['destination_account_name']) > 0) { - $destinationType = AccountType::where('type', AccountType::EXPENSE)->first(); - $destinationAccount = Account::firstOrCreateEncrypted( - [ - 'user_id' => $this->user->id, - 'account_type_id' => $destinationType->id, - 'name' => $data['destination_account_name'], - 'active' => 1, - ] - ); - - Log::debug(sprintf('destination account name is "%s", account is %d', $data['destination_account_name'], $destinationAccount->id)); - - return [ - 'source' => $sourceAccount, - 'destination' => $destinationAccount, - ]; - } - Log::debug('destination_account_name is empty, so default to cash account!'); - $destinationType = AccountType::where('type', AccountType::CASH)->first(); - $destinationAccount = Account::firstOrCreateEncrypted( - ['user_id' => $this->user->id, 'account_type_id' => $destinationType->id, 'name' => 'Cash account', 'active' => 1] - ); - - return [ - 'source' => $sourceAccount, - 'destination' => $destinationAccount, - ]; - - - } - - /** - * @param TransactionJournal $journal - * @param Account $account - * @param string $amount - * - * @throws FireflyException - */ - private function updateDestinationTransaction(TransactionJournal $journal, Account $account, string $amount) - { - // should be one: - $set = $journal->transactions()->where('amount', '>', 0)->get(); - if ($set->count() != 1) { - throw new FireflyException( - sprintf('Journal #%d has an unexpected (%d) amount of transactions with an amount more than zero.', $journal->id, $set->count()) - ); - } - /** @var Transaction $transaction */ - $transaction = $set->first(); - $transaction->amount = $amount; - $transaction->account_id = $account->id; - $transaction->save(); - - } - - /** - * @param TransactionJournal $journal - * @param Account $account - * @param string $amount - * - * @throws FireflyException - */ - private function updateSourceTransaction(TransactionJournal $journal, Account $account, string $amount) - { - // should be one: - $set = $journal->transactions()->where('amount', '<', 0)->get(); - if ($set->count() != 1) { - throw new FireflyException( - sprintf('Journal #%d has an unexpected (%d) amount of transactions with an amount less than zero.', $journal->id, $set->count()) - ); - } - /** @var Transaction $transaction */ - $transaction = $set->first(); - $transaction->amount = $amount; - $transaction->account_id = $account->id; - $transaction->save(); - - - } - - /** - * @param TransactionJournal $journal - * @param array $array - * - * @return bool - */ - private function updateTags(TransactionJournal $journal, array $array): bool - { - // create tag repository - /** @var TagRepositoryInterface $tagRepository */ - $tagRepository = app(TagRepositoryInterface::class); - - - // find or create all tags: - $tags = []; - $ids = []; - foreach ($array as $name) { - if (strlen(trim($name)) > 0) { - $tag = Tag::firstOrCreateEncrypted(['tag' => $name, 'user_id' => $journal->user_id]); - $tags[] = $tag; - $ids[] = $tag->id; - } - } - - // delete all tags connected to journal not in this array: - if (count($ids) > 0) { - DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->whereNotIn('tag_id', $ids)->delete(); - } - // if count is zero, delete them all: - if (count($ids) == 0) { - DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->delete(); - } - - // connect each tag to journal (if not yet connected): - /** @var Tag $tag */ - foreach ($tags as $tag) { - Log::debug(sprintf('Will try to connect tag #%d to journal #%d.', $tag->id, $journal->id)); - $tagRepository->connect($journal, $tag); - } - - return true; - } - - /** - * This method checks the data array and the given accounts to verify that the native amount, currency - * and possible the foreign currency and amount are properly saved. - * - * @param array $data - * @param array $accounts - * - * @return array - * @throws FireflyException - */ - private function verifyNativeAmount(array $data, array $accounts): array - { - /** @var TransactionType $transactionType */ - $transactionType = TransactionType::where('type', ucfirst($data['what']))->first(); - $submittedCurrencyId = $data['currency_id']; - - // which account to check for what the native currency is? - $check = 'source'; - if ($transactionType->type === TransactionType::DEPOSIT) { - $check = 'destination'; - } - switch ($transactionType->type) { - case TransactionType::DEPOSIT: - case TransactionType::WITHDRAWAL: - // continue: - $nativeCurrencyId = intval($accounts[$check]->getMeta('currency_id')); - - // does not match? Then user has submitted amount in a foreign currency: - if ($nativeCurrencyId !== $submittedCurrencyId) { - // store amount and submitted currency in "foreign currency" fields: - $data['foreign_amount'] = $data['amount']; - $data['foreign_currency_id'] = $submittedCurrencyId; - - // overrule the amount and currency ID fields to be the original again: - $data['amount'] = strval($data['native_amount']); - $data['currency_id'] = $nativeCurrencyId; - } - break; - case TransactionType::TRANSFER: - // source gets the original amount. - $data['amount'] = strval($data['source_amount']); - $data['currency_id'] = intval($accounts['source']->getMeta('currency_id')); - $data['foreign_amount'] = strval($data['destination_amount']); - $data['foreign_currency_id'] = intval($accounts['destination']->getMeta('currency_id')); - break; - default: - throw new FireflyException(sprintf('Cannot handle %s in verifyNativeAmount()', $transactionType->type)); - } - - return $data; - } } diff --git a/app/Repositories/Journal/JournalTasker.php b/app/Repositories/Journal/JournalTasker.php index b56abe0db8..5aacaa94ac 100644 --- a/app/Repositories/Journal/JournalTasker.php +++ b/app/Repositories/Journal/JournalTasker.php @@ -81,6 +81,8 @@ class JournalTasker implements JournalTaskerInterface ->leftJoin('account_types as source_account_types', 'source_accounts.account_type_id', '=', 'source_account_types.id') ->leftJoin('accounts as destination_accounts', 'destination.account_id', '=', 'destination_accounts.id') ->leftJoin('account_types as destination_account_types', 'destination_accounts.account_type_id', '=', 'destination_account_types.id') + ->leftJoin('transaction_currencies as native_currencies', 'transactions.transaction_currency_id', '=', 'native_currencies.id') + ->leftJoin('transaction_currencies as foreign_currencies', 'transactions.foreign_currency_id', '=', 'foreign_currencies.id') ->where('transactions.amount', '<', 0) ->whereNull('transactions.deleted_at') ->get( @@ -91,12 +93,23 @@ class JournalTasker implements JournalTaskerInterface 'source_accounts.encrypted as account_encrypted', 'source_account_types.type as account_type', 'transactions.amount', + 'transactions.foreign_amount', 'transactions.description', 'destination.id as destination_id', 'destination.account_id as destination_account_id', 'destination_accounts.name as destination_account_name', 'destination_accounts.encrypted as destination_account_encrypted', 'destination_account_types.type as destination_account_type', + 'native_currencies.id as transaction_currency_id', + 'native_currencies.decimal_places as transaction_currency_dp', + 'native_currencies.code as transaction_currency_code', + 'native_currencies.symbol as transaction_currency_symbol', + + 'foreign_currencies.id as foreign_currency_id', + 'foreign_currencies.decimal_places as foreign_currency_dp', + 'foreign_currencies.code as foreign_currency_code', + 'foreign_currencies.symbol as foreign_currency_symbol', + ] ); @@ -109,23 +122,33 @@ class JournalTasker implements JournalTaskerInterface $budget = $entry->budgets->first(); $category = $entry->categories->first(); $transaction = [ - 'source_id' => $entry->id, - 'source_amount' => $entry->amount, - 'description' => $entry->description, - 'source_account_id' => $entry->account_id, - 'source_account_name' => Steam::decrypt(intval($entry->account_encrypted), $entry->account_name), - 'source_account_type' => $entry->account_type, - 'source_account_before' => $sourceBalance, - 'source_account_after' => bcadd($sourceBalance, $entry->amount), - 'destination_id' => $entry->destination_id, - 'destination_amount' => bcmul($entry->amount, '-1'), - 'destination_account_id' => $entry->destination_account_id, - 'destination_account_type' => $entry->destination_account_type, - 'destination_account_name' => Steam::decrypt(intval($entry->destination_account_encrypted), $entry->destination_account_name), - 'destination_account_before' => $destinationBalance, - 'destination_account_after' => bcadd($destinationBalance, bcmul($entry->amount, '-1')), - 'budget_id' => is_null($budget) ? 0 : $budget->id, - 'category' => is_null($category) ? '' : $category->name, + 'source_id' => $entry->id, + 'source_amount' => $entry->amount, + 'foreign_source_amount' => $entry->foreign_amount, + 'description' => $entry->description, + 'source_account_id' => $entry->account_id, + 'source_account_name' => Steam::decrypt(intval($entry->account_encrypted), $entry->account_name), + 'source_account_type' => $entry->account_type, + 'source_account_before' => $sourceBalance, + 'source_account_after' => bcadd($sourceBalance, $entry->amount), + 'destination_id' => $entry->destination_id, + 'destination_amount' => bcmul($entry->amount, '-1'), + 'foreign_destination_amount' => is_null($entry->foreign_amount) ? null : bcmul($entry->foreign_amount, '-1'), + 'destination_account_id' => $entry->destination_account_id, + 'destination_account_type' => $entry->destination_account_type, + 'destination_account_name' => Steam::decrypt(intval($entry->destination_account_encrypted), $entry->destination_account_name), + 'destination_account_before' => $destinationBalance, + 'destination_account_after' => bcadd($destinationBalance, bcmul($entry->amount, '-1')), + 'budget_id' => is_null($budget) ? 0 : $budget->id, + 'category' => is_null($category) ? '' : $category->name, + 'transaction_currency_id' => $entry->transaction_currency_id, + 'transaction_currency_code' => $entry->transaction_currency_code, + 'transaction_currency_symbol' => $entry->transaction_currency_symbol, + 'transaction_currency_dp' => $entry->transaction_currency_dp, + 'foreign_currency_id' => $entry->foreign_currency_id, + 'foreign_currency_code' => $entry->foreign_currency_code, + 'foreign_currency_symbol' => $entry->foreign_currency_symbol, + 'foreign_currency_dp' => $entry->foreign_currency_dp, ]; if ($entry->destination_account_type === AccountType::CASH) { $transaction['destination_account_name'] = ''; diff --git a/app/Repositories/Journal/SupportJournalsTrait.php b/app/Repositories/Journal/SupportJournalsTrait.php new file mode 100644 index 0000000000..df57879971 --- /dev/null +++ b/app/Repositories/Journal/SupportJournalsTrait.php @@ -0,0 +1,246 @@ + null, + 'destination' => null, + ]; + + Log::debug(sprintf('Going to store accounts for type %s', $type->type)); + switch ($type->type) { + case TransactionType::WITHDRAWAL: + $accounts = $this->storeWithdrawalAccounts($user, $data); + break; + + case TransactionType::DEPOSIT: + $accounts = $this->storeDepositAccounts($user, $data); + + break; + case TransactionType::TRANSFER: + $accounts['source'] = Account::where('user_id', $user->id)->where('id', $data['source_account_id'])->first(); + $accounts['destination'] = Account::where('user_id', $user->id)->where('id', $data['destination_account_id'])->first(); + break; + default: + throw new FireflyException(sprintf('Did not recognise transaction type "%s".', $type->type)); + } + + if (is_null($accounts['source'])) { + Log::error('"source"-account is null, so we cannot continue!', ['data' => $data]); + throw new FireflyException('"source"-account is null, so we cannot continue!'); + } + + if (is_null($accounts['destination'])) { + Log::error('"destination"-account is null, so we cannot continue!', ['data' => $data]); + throw new FireflyException('"destination"-account is null, so we cannot continue!'); + + } + + + return $accounts; + } + + /** + * @param TransactionJournal $journal + * @param int $budgetId + */ + protected function storeBudgetWithJournal(TransactionJournal $journal, int $budgetId) + { + if (intval($budgetId) > 0 && $journal->transactionType->type === TransactionType::WITHDRAWAL) { + /** @var \FireflyIII\Models\Budget $budget */ + $budget = Budget::find($budgetId); + $journal->budgets()->save($budget); + } + } + + /** + * @param TransactionJournal $journal + * @param string $category + */ + protected function storeCategoryWithJournal(TransactionJournal $journal, string $category) + { + if (strlen($category) > 0) { + $category = Category::firstOrCreateEncrypted(['name' => $category, 'user_id' => $journal->user_id]); + $journal->categories()->save($category); + } + } + + /** + * @param User $user + * @param array $data + * + * @return array + */ + protected function storeDepositAccounts(User $user, array $data): array + { + Log::debug('Now in storeDepositAccounts().'); + $destinationAccount = Account::where('user_id', $user->id)->where('id', $data['destination_account_id'])->first(['accounts.*']); + + Log::debug(sprintf('Destination account is #%d ("%s")', $destinationAccount->id, $destinationAccount->name)); + + if (strlen($data['source_account_name']) > 0) { + $sourceType = AccountType::where('type', 'Revenue account')->first(); + $sourceAccount = Account::firstOrCreateEncrypted( + ['user_id' => $user->id, 'account_type_id' => $sourceType->id, 'name' => $data['source_account_name'], 'active' => 1] + ); + + Log::debug(sprintf('source account name is "%s", account is %d', $data['source_account_name'], $sourceAccount->id)); + + return [ + 'source' => $sourceAccount, + 'destination' => $destinationAccount, + ]; + } + + Log::debug('source_account_name is empty, so default to cash account!'); + + $sourceType = AccountType::where('type', AccountType::CASH)->first(); + $sourceAccount = Account::firstOrCreateEncrypted( + ['user_id' => $user->id, 'account_type_id' => $sourceType->id, 'name' => 'Cash account', 'active' => 1] + ); + + return [ + 'source' => $sourceAccount, + 'destination' => $destinationAccount, + ]; + } + + /** + * @param User $user + * @param array $data + * + * @return array + */ + protected function storeWithdrawalAccounts(User $user, array $data): array + { + Log::debug('Now in storeWithdrawalAccounts().'); + $sourceAccount = Account::where('user_id', $user->id)->where('id', $data['source_account_id'])->first(['accounts.*']); + + Log::debug(sprintf('Source account is #%d ("%s")', $sourceAccount->id, $sourceAccount->name)); + + if (strlen($data['destination_account_name']) > 0) { + $destinationType = AccountType::where('type', AccountType::EXPENSE)->first(); + $destinationAccount = Account::firstOrCreateEncrypted( + [ + 'user_id' => $user->id, + 'account_type_id' => $destinationType->id, + 'name' => $data['destination_account_name'], + 'active' => 1, + ] + ); + + Log::debug(sprintf('destination account name is "%s", account is %d', $data['destination_account_name'], $destinationAccount->id)); + + return [ + 'source' => $sourceAccount, + 'destination' => $destinationAccount, + ]; + } + Log::debug('destination_account_name is empty, so default to cash account!'); + $destinationType = AccountType::where('type', AccountType::CASH)->first(); + $destinationAccount = Account::firstOrCreateEncrypted( + ['user_id' => $user->id, 'account_type_id' => $destinationType->id, 'name' => 'Cash account', 'active' => 1] + ); + + return [ + 'source' => $sourceAccount, + 'destination' => $destinationAccount, + ]; + } + + /** + * This method checks the data array and the given accounts to verify that the native amount, currency + * and possible the foreign currency and amount are properly saved. + * + * @param array $data + * @param array $accounts + * + * @return array + * @throws FireflyException + */ + protected function verifyNativeAmount(array $data, array $accounts): array + { + /** @var TransactionType $transactionType */ + $transactionType = TransactionType::where('type', ucfirst($data['what']))->first(); + $submittedCurrencyId = $data['currency_id']; + $data['foreign_amount'] = null; + $data['foreign_currency_id'] = null; + + // which account to check for what the native currency is? + $check = 'source'; + if ($transactionType->type === TransactionType::DEPOSIT) { + $check = 'destination'; + } + switch ($transactionType->type) { + case TransactionType::DEPOSIT: + case TransactionType::WITHDRAWAL: + // continue: + $nativeCurrencyId = intval($accounts[$check]->getMeta('currency_id')); + + // does not match? Then user has submitted amount in a foreign currency: + if ($nativeCurrencyId !== $submittedCurrencyId) { + // store amount and submitted currency in "foreign currency" fields: + $data['foreign_amount'] = $data['amount']; + $data['foreign_currency_id'] = $submittedCurrencyId; + + // overrule the amount and currency ID fields to be the original again: + $data['amount'] = strval($data['native_amount']); + $data['currency_id'] = $nativeCurrencyId; + } + break; + case TransactionType::TRANSFER: + $sourceCurrencyId = intval($accounts['source']->getMeta('currency_id')); + $destinationCurrencyId = intval($accounts['destination']->getMeta('currency_id')); + $data['amount'] = strval($data['source_amount']); + $data['currency_id'] = intval($accounts['source']->getMeta('currency_id')); + + if ($sourceCurrencyId !== $destinationCurrencyId) { + // accounts have different id's, save this info: + $data['foreign_amount'] = strval($data['destination_amount']); + $data['foreign_currency_id'] = $destinationCurrencyId; + } + + break; + default: + throw new FireflyException(sprintf('Cannot handle %s in verifyNativeAmount()', $transactionType->type)); + } + + return $data; + } +} diff --git a/app/Repositories/Journal/UpdateJournalsTrait.php b/app/Repositories/Journal/UpdateJournalsTrait.php new file mode 100644 index 0000000000..d4264818a6 --- /dev/null +++ b/app/Repositories/Journal/UpdateJournalsTrait.php @@ -0,0 +1,156 @@ +transactions()->where('amount', '>', 0)->get(); + if ($set->count() !== 1) { + throw new FireflyException(sprintf('Journal #%d has %d transactions with an amount more than zero.', $journal->id, $set->count())); + } + /** @var Transaction $transaction */ + $transaction = $set->first(); + $transaction->amount = app('steam')->positive($data['amount']); + $transaction->transaction_currency_id = $data['currency_id']; + $transaction->foreign_amount = is_null($data['foreign_amount']) ? null : app('steam')->positive($data['foreign_amount']); + $transaction->foreign_currency_id = $data['foreign_currency_id']; + $transaction->account_id = $account->id; + $transaction->save(); + + } + + /** + * @param TransactionJournal $journal + * @param Account $account + * @param array $data + * + * @throws FireflyException + */ + protected function updateSourceTransaction(TransactionJournal $journal, Account $account, array $data) + { + // should be one: + $set = $journal->transactions()->where('amount', '<', 0)->get(); + if ($set->count() !== 1) { + throw new FireflyException(sprintf('Journal #%d has %d transactions with an amount more than zero.', $journal->id, $set->count())); + } + /** @var Transaction $transaction */ + $transaction = $set->first(); + $transaction->amount = bcmul(app('steam')->positive($data['amount']), '-1'); + $transaction->transaction_currency_id = $data['currency_id']; + $transaction->foreign_amount = is_null($data['foreign_amount']) ? null : bcmul(app('steam')->positive($data['foreign_amount']), '-1'); + $transaction->foreign_currency_id = $data['foreign_currency_id']; + $transaction->account_id = $account->id; + $transaction->save(); + } + + /** + * @param TransactionJournal $journal + * @param array $array + * + * @return bool + */ + protected function updateTags(TransactionJournal $journal, array $array): bool + { + // create tag repository + /** @var TagRepositoryInterface $tagRepository */ + $tagRepository = app(TagRepositoryInterface::class); + + + // find or create all tags: + $tags = []; + $ids = []; + foreach ($array as $name) { + if (strlen(trim($name)) > 0) { + $tag = Tag::firstOrCreateEncrypted(['tag' => $name, 'user_id' => $journal->user_id]); + $tags[] = $tag; + $ids[] = $tag->id; + } + } + + // delete all tags connected to journal not in this array: + if (count($ids) > 0) { + DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->whereNotIn('tag_id', $ids)->delete(); + } + // if count is zero, delete them all: + if (count($ids) === 0) { + DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->delete(); + } + + // connect each tag to journal (if not yet connected): + /** @var Tag $tag */ + foreach ($tags as $tag) { + Log::debug(sprintf('Will try to connect tag #%d to journal #%d.', $tag->id, $journal->id)); + $tagRepository->connect($journal, $tag); + } + + return true; + } +} diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index e721c71b63..6c05daa897 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -240,10 +240,12 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface */ public function getPiggyBanksWithAmount(): Collection { - $set = $this->getPiggyBanks(); + $currency = Amount::getDefaultCurrency(); + $set = $this->getPiggyBanks(); foreach ($set as $piggy) { $currentAmount = $piggy->currentRelevantRep()->currentamount ?? '0'; - $piggy->name = $piggy->name . ' (' . Amount::format($currentAmount, false) . ')'; + + $piggy->name = $piggy->name . ' (' . Amount::formatAnything($currency, $currentAmount, false) . ')'; } return $set; diff --git a/app/Repositories/Rule/RuleRepository.php b/app/Repositories/Rule/RuleRepository.php index 47a848427c..5d3a1f2d47 100644 --- a/app/Repositories/Rule/RuleRepository.php +++ b/app/Repositories/Rule/RuleRepository.php @@ -236,7 +236,7 @@ class RuleRepository implements RuleRepositoryInterface $rule->rule_group_id = $data['rule_group_id']; $rule->order = ($order + 1); $rule->active = 1; - $rule->stop_processing = intval($data['stop_processing']) == 1; + $rule->stop_processing = intval($data['stop_processing']) === 1; $rule->title = $data['title']; $rule->description = strlen($data['description']) > 0 ? $data['description'] : null; diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index 63ee5b408b..581fb72ebf 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -47,7 +47,7 @@ class TagRepository implements TagRepositoryInterface * Already connected: */ if ($journal->tags()->find($tag->id)) { - Log::error(sprintf('Cannot find tag #%d', $tag->id)); + Log::info(sprintf('Tag #%d is already connected to journal #%d.', $tag->id, $journal->id)); return false; } @@ -282,7 +282,7 @@ class TagRepository implements TagRepositoryInterface * changed to an advancePayment. */ - if ($tag->tagMode == 'balancingAct' || $tag->tagMode == 'nothing') { + if ($tag->tagMode === 'balancingAct' || $tag->tagMode === 'nothing') { foreach ($tag->transactionjournals as $journal) { if ($journal->isTransfer()) { return false; @@ -394,7 +394,7 @@ class TagRepository implements TagRepositoryInterface } // if already has transaction journals, must match ALL asset account id's: - if ($deposits > 0 || $withdrawals == 1) { + if ($deposits > 0 || $withdrawals === 1) { Log::debug('Need to match all asset accounts.'); return $this->matchAll($journal, $tag); diff --git a/app/Rules/Actions/RemoveTag.php b/app/Rules/Actions/RemoveTag.php index ca9912f5f0..525579f0ed 100644 --- a/app/Rules/Actions/RemoveTag.php +++ b/app/Rules/Actions/RemoveTag.php @@ -52,7 +52,7 @@ class RemoveTag implements ActionInterface /** @var Tag $tag */ $tag = $journal->user->tags()->get()->filter( function (Tag $tag) use ($name) { - return $tag->tag == $name; + return $tag->tag === $name; } )->first(); diff --git a/app/Rules/Actions/SetBudget.php b/app/Rules/Actions/SetBudget.php index 62b530b8c8..599f0214da 100644 --- a/app/Rules/Actions/SetBudget.php +++ b/app/Rules/Actions/SetBudget.php @@ -56,7 +56,7 @@ class SetBudget implements ActionInterface $budgets = $repository->getActiveBudgets(); $budget = $budgets->filter( function (Budget $current) use ($search) { - return $current->name == $search; + return $current->name === $search; } )->first(); if (is_null($budget)) { @@ -65,7 +65,7 @@ class SetBudget implements ActionInterface return true; } - if ($journal->transactionType->type == TransactionType::TRANSFER) { + if ($journal->transactionType->type === TransactionType::TRANSFER) { Log::debug(sprintf('RuleAction SetBudget could not set budget of journal #%d to "%s" because journal is a transfer.', $journal->id, $search)); return true; diff --git a/app/Rules/Processor.php b/app/Rules/Processor.php index 579521af61..ae0ecb1fd8 100644 --- a/app/Rules/Processor.php +++ b/app/Rules/Processor.php @@ -254,7 +254,7 @@ final class Processor } } - $result = ($hitTriggers == $foundTriggers && $foundTriggers > 0); + $result = ($hitTriggers === $foundTriggers && $foundTriggers > 0); Log::debug('Result of triggered()', ['hitTriggers' => $hitTriggers, 'foundTriggers' => $foundTriggers, 'result' => $result]); return $result; diff --git a/app/Rules/Triggers/HasAttachment.php b/app/Rules/Triggers/HasAttachment.php index 4fc3bd720f..6fc86c6a6c 100644 --- a/app/Rules/Triggers/HasAttachment.php +++ b/app/Rules/Triggers/HasAttachment.php @@ -58,4 +58,4 @@ class HasAttachment extends AbstractTrigger implements TriggerInterface return false; } -} \ No newline at end of file +} diff --git a/app/Services/Currency/ExchangeRateInterface.php b/app/Services/Currency/ExchangeRateInterface.php index a37133db9a..69a8040a5d 100644 --- a/app/Services/Currency/ExchangeRateInterface.php +++ b/app/Services/Currency/ExchangeRateInterface.php @@ -35,4 +35,4 @@ interface ExchangeRateInterface */ public function setUser(User $user); -} \ No newline at end of file +} diff --git a/app/Services/Currency/FixerIO.php b/app/Services/Currency/FixerIO.php index 50a63036fa..7a3290bb96 100644 --- a/app/Services/Currency/FixerIO.php +++ b/app/Services/Currency/FixerIO.php @@ -61,9 +61,11 @@ class FixerIO implements ExchangeRateInterface /** * @param User $user + * + * @return mixed|void */ public function setUser(User $user) { $this->user = $user; } -} \ No newline at end of file +} diff --git a/app/Support/Amount.php b/app/Support/Amount.php index 32e42cf3bb..071c3e48b6 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -14,9 +14,11 @@ declare(strict_types=1); namespace FireflyIII\Support; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Models\Transaction; +use FireflyIII\Models\Transaction as TransactionModel; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use FireflyIII\User; use Illuminate\Support\Collection; use Preferences as Prefs; @@ -27,6 +29,7 @@ use Preferences as Prefs; */ class Amount { + /** * bool $sepBySpace is $localeconv['n_sep_by_space'] * int $signPosn = $localeconv['n_sign_posn'] @@ -101,17 +104,6 @@ class Amount return $format; } - /** - * @param string $amount - * @param bool $coloured - * - * @return string - */ - public function format(string $amount, bool $coloured = true): string - { - return $this->formatAnything($this->getDefaultCurrency(), $amount, $coloured); - } - /** * This method will properly format the given number, in color or "black and white", * as a currency, given two things: the currency required and the current locale. @@ -159,49 +151,6 @@ class Amount return $result; } - /** - * Used in many places (unfortunately). - * - * @param string $currencyCode - * @param string $amount - * @param bool $coloured - * - * @return string - */ - public function formatByCode(string $currencyCode, string $amount, bool $coloured = true): string - { - $currency = TransactionCurrency::where('code', $currencyCode)->first(); - - return $this->formatAnything($currency, $amount, $coloured); - } - - /** - * - * @param \FireflyIII\Models\TransactionJournal $journal - * @param bool $coloured - * - * @return string - */ - public function formatJournal(TransactionJournal $journal, bool $coloured = true): string - { - $currency = $journal->transactionCurrency; - - return $this->formatAnything($currency, $journal->amount(), $coloured); - } - - /** - * @param Transaction $transaction - * @param bool $coloured - * - * @return string - */ - public function formatTransaction(Transaction $transaction, bool $coloured = true) - { - $currency = $transaction->transactionJournal->transactionCurrency; - - return $this->formatAnything($currency, strval($transaction->amount), $coloured); - } - /** * @return Collection */ @@ -256,17 +205,31 @@ class Amount } /** - * @return TransactionCurrency + * @return \FireflyIII\Models\TransactionCurrency * @throws FireflyException */ public function getDefaultCurrency(): TransactionCurrency + { + $user = auth()->user(); + + return $this->getDefaultCurrencyByUser($user); + } + + /** + * @param User $user + * + * @return \FireflyIII\Models\TransactionCurrency + * @throws FireflyException + */ + public function getDefaultCurrencyByUser(User $user): TransactionCurrency { $cache = new CacheProperties; $cache->addProperty('getDefaultCurrency'); + $cache->addProperty($user->id); if ($cache->has()) { return $cache->get(); // @codeCoverageIgnore } - $currencyPreference = Prefs::get('currencyPreference', config('firefly.default_currency', 'EUR')); + $currencyPreference = Prefs::getForUser($user, 'currencyPreference', config('firefly.default_currency', 'EUR')); $currency = TransactionCurrency::where('code', $currencyPreference->data)->first(); if (is_null($currency)) { throw new FireflyException(sprintf('No currency found with code "%s"', $currencyPreference->data)); @@ -295,4 +258,84 @@ class Amount 'zero' => $positive, ]; } + + /** + * @param TransactionJournal $journal + * @param bool $coloured + * + * @return string + */ + public function journalAmount(TransactionJournal $journal, bool $coloured = true): string + { + $amounts = []; + $transactions = $journal->transactions()->where('amount', '>', 0)->get(); + /** @var TransactionModel $transaction */ + foreach ($transactions as $transaction) { + // model some fields to fit "transactionAmount()": + $transaction->transaction_amount = $transaction->amount; + $transaction->transaction_foreign_amount = $transaction->foreign_amount; + $transaction->transaction_type_type = $journal->transactionType->type; + $transaction->transaction_currency_symbol = $transaction->transactionCurrency->symbol; + $transaction->transaction_currency_dp = $transaction->transactionCurrency->decimal_places; + if (!is_null($transaction->foreign_currency_id)) { + $transaction->foreign_currency_symbol = $transaction->foreignCurrency->symbol; + $transaction->foreign_currency_dp = $transaction->foreignCurrency->decimal_places; + } + + $amounts[] = $this->transactionAmount($transaction, $coloured); + } + + return join(' / ', $amounts); + + } + + /** + * This formats a transaction, IF that transaction has been "collected" using the JournalCollector. + * + * @param TransactionModel $transaction + * @param bool $coloured + * + * @return string + */ + public function transactionAmount(TransactionModel $transaction, bool $coloured = true): string + { + $amount = bcmul(app('steam')->positive(strval($transaction->transaction_amount)), '-1'); + $format = '%s'; + + if ($transaction->transaction_type_type === TransactionType::DEPOSIT) { + $amount = bcmul($amount, '-1'); + } + + if ($transaction->transaction_type_type === TransactionType::TRANSFER) { + $amount = app('steam')->positive($amount); + $coloured = false; + $format = '%s'; + } + if($transaction->transaction_type_type === TransactionType::OPENING_BALANCE) { + $amount = strval($transaction->transaction_amount); + } + + $currency = new TransactionCurrency; + $currency->symbol = $transaction->transaction_currency_symbol; + $currency->decimal_places = $transaction->transaction_currency_dp; + $str = sprintf($format, $this->formatAnything($currency, $amount, $coloured)); + + + if (!is_null($transaction->transaction_foreign_amount)) { + $amount = strval($transaction->transaction_foreign_amount); + + if ($transaction->transaction_type_type === TransactionType::TRANSFER) { + $amount = app('steam')->positive($amount); + $coloured = false; + $format = '%s'; + } + + $currency = new TransactionCurrency; + $currency->symbol = $transaction->foreign_currency_symbol; + $currency->decimal_places = $transaction->foreign_currency_dp; + $str .= ' (' . sprintf($format, $this->formatAnything($currency, $amount, $coloured)) . ')'; + } + + return $str; + } } diff --git a/app/Support/Binder/CurrencyCode.php b/app/Support/Binder/CurrencyCode.php index 4616ce5ef2..39b0a73915 100644 --- a/app/Support/Binder/CurrencyCode.php +++ b/app/Support/Binder/CurrencyCode.php @@ -36,4 +36,4 @@ class CurrencyCode implements BinderInterface } throw new NotFoundHttpException; } -} \ No newline at end of file +} diff --git a/app/Support/Binder/JournalList.php b/app/Support/Binder/JournalList.php index cc31681323..3b5678f886 100644 --- a/app/Support/Binder/JournalList.php +++ b/app/Support/Binder/JournalList.php @@ -37,15 +37,8 @@ class JournalList implements BinderInterface $ids = explode(',', $value); /** @var \Illuminate\Support\Collection $object */ $object = TransactionJournal::whereIn('transaction_journals.id', $ids) - ->expanded() ->where('transaction_journals.user_id', auth()->user()->id) - ->get( - [ - 'transaction_journals.*', - 'transaction_types.type AS transaction_type_type', - 'transaction_currencies.code AS transaction_currency_code', - ] - ); + ->get(['transaction_journals.*',]); if ($object->count() > 0) { return $object; diff --git a/app/Support/CacheProperties.php b/app/Support/CacheProperties.php index 555fe427b0..d83e8e73a6 100644 --- a/app/Support/CacheProperties.php +++ b/app/Support/CacheProperties.php @@ -75,7 +75,7 @@ class CacheProperties */ public function has(): bool { - if (getenv('APP_ENV') == 'testing') { + if (getenv('APP_ENV') === 'testing') { return false; } $this->md5(); @@ -119,8 +119,6 @@ class CacheProperties $this->md5 .= json_encode($property); } - Log::debug(sprintf('Cache string is %s', $this->md5)); $this->md5 = md5($this->md5); - Log::debug(sprintf('Cache MD5 is %s', $this->md5)); } } diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index dab2ed286d..c1dc2fc174 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -565,6 +565,19 @@ class ExpandedForm unset($options['currency']); unset($options['placeholder']); + // perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount) + $preFilled = session('preFilled'); + $key = 'amount_currency_id_' . $name; + $sentCurrencyId = isset($preFilled[$key]) ? intval($preFilled[$key]) : $defaultCurrency->id; + + // find this currency in set of currencies: + foreach ($currencies as $currency) { + if ($currency->id === $sentCurrencyId) { + $defaultCurrency = $currency; + break; + } + } + // make sure value is formatted nicely: if (!is_null($value) && $value !== '') { $value = round($value, $defaultCurrency->decimal_places); diff --git a/app/Support/Import/Configuration/ConfigurationInterface.php b/app/Support/Import/Configuration/ConfigurationInterface.php new file mode 100644 index 0000000000..7f13ebe167 --- /dev/null +++ b/app/Support/Import/Configuration/ConfigurationInterface.php @@ -0,0 +1,46 @@ +getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + $delimiters = [ + ',' => trans('form.csv_comma'), + ';' => trans('form.csv_semicolon'), + 'tab' => trans('form.csv_tab'), + ]; + + $specifics = []; + + // collect specifics. + foreach (config('csv.import_specifics') as $name => $className) { + $specifics[$name] = [ + 'name' => $className::getName(), + 'description' => $className::getDescription(), + ]; + } + + $data = [ + 'accounts' => ExpandedForm::makeSelectList($accounts), + 'specifix' => [], + 'delimiters' => $delimiters, + 'specifics' => $specifics, + ]; + + return $data; + } + + /** + * @param ImportJob $job + * + * @return ConfigurationInterface + */ + public function setJob(ImportJob $job): ConfigurationInterface + { + $this->job = $job; + + return $this; + } + + /** + * Store the result. + * + * @param array $data + * + * @return bool + */ + public function storeConfiguration(array $data): bool + { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $importId = $data['csv_import_account'] ?? 0; + $account = $repository->find(intval($importId)); + + $hasHeaders = isset($data['has_headers']) && intval($data['has_headers']) === 1 ? true : false; + $config = $this->job->configuration; + $config['initial-config-complete'] = true; + $config['has-headers'] = $hasHeaders; + $config['date-format'] = $data['date_format']; + $config['delimiter'] = $data['csv_delimiter']; + $config['delimiter'] = $config['delimiter'] === 'tab' ? "\t" : $config['delimiter']; + + Log::debug('Entered import account.', ['id' => $importId]); + + + if (!is_null($account->id)) { + Log::debug('Found account.', ['id' => $account->id, 'name' => $account->name]); + $config['import-account'] = $account->id; + } + + if (is_null($account->id)) { + Log::error('Could not find anything for csv_import_account.', ['id' => $importId]); + } + + $config = $this->storeSpecifics($data, $config); + $this->job->configuration = $config; + $this->job->save(); + + return true; + } + + /** + * @param array $data + * @param array $config + * + * @return array + */ + private function storeSpecifics(array $data, array $config): array + { + // loop specifics. + if (isset($data['specifics']) && is_array($data['specifics'])) { + foreach ($data['specifics'] as $name => $enabled) { + // verify their content. + $className = sprintf('FireflyIII\Import\Specifics\%s', $name); + if (class_exists($className)) { + $config['specifics'][$name] = 1; + } + } + } + + return $config; + } +} diff --git a/app/Support/Import/Configuration/Csv/Map.php b/app/Support/Import/Configuration/Csv/Map.php new file mode 100644 index 0000000000..f9cae51a0c --- /dev/null +++ b/app/Support/Import/Configuration/Csv/Map.php @@ -0,0 +1,270 @@ +configuration = $this->job->configuration; + $this->getMappableColumns(); + + // in order to actually map we also need all possible values from the CSV file. + $content = $this->job->uploadFileContents(); + /** @var Reader $reader */ + $reader = Reader::createFromString($content); + $reader->setDelimiter($this->configuration['delimiter']); + $offset = $this->configuration['has-headers'] ? 1 : 0; + $results = $reader->setOffset($offset)->fetch(); + $this->validSpecifics = array_keys(config('csv.import_specifics')); + $indexes = array_keys($this->data); + $rowIndex = 0; + foreach ($results as $rowIndex => $row) { + + $row = $this->runSpecifics($row); + + //do something here + + foreach ($indexes as $index) { // this is simply 1, 2, 3, etc. + if (!isset($row[$index])) { + // don't really know how to handle this. Just skip, for now. + continue; + } + $value = $row[$index]; + if (strlen($value) > 0) { + + // we can do some preprocessing here, + // which is exclusively to fix the tags: + if (!is_null($this->data[$index]['preProcessMap']) && strlen($this->data[$index]['preProcessMap']) > 0) { + /** @var PreProcessorInterface $preProcessor */ + $preProcessor = app($this->data[$index]['preProcessMap']); + $result = $preProcessor->run($value); + $this->data[$index]['values'] = array_merge($this->data[$index]['values'], $result); + + Log::debug($rowIndex . ':' . $index . 'Value before preprocessor', ['value' => $value]); + Log::debug($rowIndex . ':' . $index . 'Value after preprocessor', ['value-new' => $result]); + Log::debug($rowIndex . ':' . $index . 'Value after joining', ['value-complete' => $this->data[$index]['values']]); + + + continue; + } + + $this->data[$index]['values'][] = $value; + } + } + } + foreach ($this->data as $index => $entry) { + $this->data[$index]['values'] = array_unique($this->data[$index]['values']); + asort($this->data[$index]['values']); + } + + // save number of rows, thus number of steps, in job: + $steps = $rowIndex * 5; + $extended = $this->job->extended_status; + $extended['steps'] = $steps; + $this->job->extended_status = $extended; + $this->job->save(); + + return $this->data; + + } + + /** + * @param ImportJob $job + * + * @return ConfigurationInterface + */ + public function setJob(ImportJob $job): ConfigurationInterface + { + $this->job = $job; + + return $this; + } + + /** + * Store the result. + * + * @param array $data + * + * @return bool + */ + public function storeConfiguration(array $data): bool + { + $config = $this->job->configuration; + + foreach ($data['mapping'] as $index => $data) { + $config['column-mapping-config'][$index] = []; + foreach ($data as $value => $mapId) { + $mapId = intval($mapId); + if ($mapId !== 0) { + $config['column-mapping-config'][$index][$value] = intval($mapId); + } + } + } + + // set thing to be completed. + $config['column-mapping-complete'] = true; + $this->job->configuration = $config; + $this->job->save(); + + return true; + } + + /** + * @param string $column + * + * @return MapperInterface + */ + private function createMapper(string $column): MapperInterface + { + $mapperClass = config('csv.import_roles.' . $column . '.mapper'); + $mapperName = sprintf('\\FireflyIII\\Import\Mapper\\%s', $mapperClass); + /** @var MapperInterface $mapper */ + $mapper = new $mapperName; + + return $mapper; + } + + /** + * @return bool + */ + private function getMappableColumns(): bool + { + $config = $this->job->configuration; + + /** + * @var int $index + * @var bool $mustBeMapped + */ + foreach ($config['column-do-mapping'] as $index => $mustBeMapped) { + $column = $this->validateColumnName($config['column-roles'][$index] ?? '_ignore'); + $shouldMap = $this->shouldMapColumn($column, $mustBeMapped); + if ($shouldMap) { + + + // create configuration entry for this specific column and add column to $this->data array for later processing. + $this->data[$index] = [ + 'name' => $column, + 'index' => $index, + 'options' => $this->createMapper($column)->getMap(), + 'preProcessMap' => $this->getPreProcessorName($column), + 'values' => [], + ]; + } + } + + return true; + + } + + /** + * @param string $column + * + * @return string + */ + private function getPreProcessorName(string $column): string + { + $name = ''; + $hasPreProcess = config('csv.import_roles.' . $column . '.pre-process-map'); + $preProcessClass = config('csv.import_roles.' . $column . '.pre-process-mapper'); + + if (!is_null($hasPreProcess) && $hasPreProcess === true && !is_null($preProcessClass)) { + $name = sprintf('\\FireflyIII\\Import\\MapperPreProcess\\%s', $preProcessClass); + } + + return $name; + } + + /** + * @param array $row + * + * @return array + * @throws FireflyException + */ + private function runSpecifics(array $row): array + { + // run specifics here: + // and this is the point where the specifix go to work. + foreach ($this->configuration['specifics'] as $name => $enabled) { + + if (!in_array($name, $this->validSpecifics)) { + throw new FireflyException(sprintf('"%s" is not a valid class name', $name)); + } + $class = config('csv.import_specifics.' . $name); + /** @var SpecificInterface $specific */ + $specific = app($class); + + // it returns the row, possibly modified: + $row = $specific->run($row); + } + + return $row; + } + + /** + * @param string $column + * @param bool $mustBeMapped + * + * @return bool + */ + private function shouldMapColumn(string $column, bool $mustBeMapped): bool + { + $canBeMapped = config('csv.import_roles.' . $column . '.mappable'); + + return ($canBeMapped && $mustBeMapped); + } + + /** + * @param string $column + * + * @return string + * @throws FireflyException + */ + private function validateColumnName(string $column): string + { + // is valid column? + $validColumns = array_keys(config('csv.import_roles')); + if (!in_array($column, $validColumns)) { + throw new FireflyException(sprintf('"%s" is not a valid column.', $column)); + } + + return $column; + } +} diff --git a/app/Support/Import/Configuration/Csv/Roles.php b/app/Support/Import/Configuration/Csv/Roles.php new file mode 100644 index 0000000000..445628aeab --- /dev/null +++ b/app/Support/Import/Configuration/Csv/Roles.php @@ -0,0 +1,267 @@ +job->configuration; + $content = $this->job->uploadFileContents(); + + // create CSV reader. + $reader = Reader::createFromString($content); + $reader->setDelimiter($config['delimiter']); + $start = $config['has-headers'] ? 1 : 0; + $end = $start + config('csv.example_rows'); + + // set data: + $this->data = [ + 'examples' => [], + 'roles' => $this->getRoles(), + 'total' => 0, + 'headers' => $config['has-headers'] ? $reader->fetchOne(0) : [], + ]; + + while ($start < $end) { + $row = $reader->fetchOne($start); + $row = $this->processSpecifics($row); + $count = count($row); + $this->data['total'] = $count > $this->data['total'] ? $count : $this->data['total']; + $this->processRow($row); + $start++; + } + + $this->updateColumCount(); + $this->makeExamplesUnique(); + + return $this->data; + } + + /** + * @param ImportJob $job + * + * @return ConfigurationInterface + */ + public function setJob(ImportJob $job): ConfigurationInterface + { + $this->job = $job; + + return $this; + } + + /** + * Store the result. + * + * @param array $data + * + * @return bool + */ + public function storeConfiguration(array $data): bool + { + Log::debug('Now in storeConfiguration of Roles.'); + $config = $this->job->configuration; + $count = $config['column-count']; + for ($i = 0; $i < $count; $i++) { + $role = $data['role'][$i] ?? '_ignore'; + $mapping = isset($data['map'][$i]) && $data['map'][$i] === '1' ? true : false; + $config['column-roles'][$i] = $role; + $config['column-do-mapping'][$i] = $mapping; + Log::debug(sprintf('Column %d has been given role %s', $i, $role)); + } + + + $this->job->configuration = $config; + $this->job->save(); + + $this->ignoreUnmappableColumns(); + $this->setRolesComplete(); + $this->isMappingNecessary(); + + + return true; + } + + /** + * @return array + */ + private function getRoles(): array + { + $roles = []; + foreach (array_keys(config('csv.import_roles')) as $role) { + $roles[$role] = trans('csv.column_' . $role); + } + + return $roles; + + } + + /** + * @return bool + */ + private function ignoreUnmappableColumns(): bool + { + $config = $this->job->configuration; + $count = $config['column-count']; + for ($i = 0; $i < $count; $i++) { + $role = $config['column-roles'][$i] ?? '_ignore'; + $mapping = $config['column-do-mapping'][$i] ?? false; + + if ($role === '_ignore' && $mapping === true) { + $mapping = false; + Log::debug(sprintf('Column %d has type %s so it cannot be mapped.', $i, $role)); + } + $config['column-do-mapping'][$i] = $mapping; + } + + $this->job->configuration = $config; + $this->job->save(); + + return true; + + } + + /** + * @return bool + */ + private function isMappingNecessary() + { + $config = $this->job->configuration; + $count = $config['column-count']; + $toBeMapped = 0; + for ($i = 0; $i < $count; $i++) { + $mapping = $config['column-do-mapping'][$i] ?? false; + if ($mapping === true) { + $toBeMapped++; + } + } + Log::debug(sprintf('Found %d columns that need mapping.', $toBeMapped)); + if ($toBeMapped === 0) { + // skip setting of map, because none need to be mapped: + $config['column-mapping-complete'] = true; + $this->job->configuration = $config; + $this->job->save(); + } + + return true; + } + + /** + * make unique example data + */ + private function makeExamplesUnique(): bool + { + foreach ($this->data['examples'] as $index => $values) { + $this->data['examples'][$index] = array_unique($values); + } + + return true; + } + + /** + * @param array $row + * + * @return bool + */ + private function processRow(array $row): bool + { + foreach ($row as $index => $value) { + $value = trim($value); + if (strlen($value) > 0) { + $this->data['examples'][$index][] = $value; + } + } + + return true; + } + + /** + * run specifics here: + * and this is the point where the specifix go to work. + * + * @param array $row + * + * @return array + */ + private function processSpecifics(array $row): array + { + foreach ($this->job->configuration['specifics'] as $name => $enabled) { + /** @var SpecificInterface $specific */ + $specific = app('FireflyIII\Import\Specifics\\' . $name); + $row = $specific->run($row); + } + + return $row; + } + + /** + * @return bool + */ + private function setRolesComplete(): bool + { + $config = $this->job->configuration; + $count = $config['column-count']; + $assigned = 0; + $hasAmount = false; + for ($i = 0; $i < $count; $i++) { + $role = $config['column-roles'][$i] ?? '_ignore'; + if ($role !== '_ignore') { + $assigned++; + } + if ($role === 'amount') { + $hasAmount = true; + } + } + if ($assigned > 0 && $hasAmount) { + $config['column-roles-complete'] = true; + $this->job->configuration = $config; + $this->job->save(); + } + + return true; + } + + /** + * @return bool + */ + private function updateColumCount(): bool + { + $config = $this->job->configuration; + $count = $this->data['total']; + $config['column-count'] = $count; + $this->job->configuration = $config; + $this->job->save(); + + return true; + } +} diff --git a/app/Support/Models/TransactionJournalTrait.php b/app/Support/Models/TransactionJournalTrait.php index c9cea3ec49..99f87b8684 100644 --- a/app/Support/Models/TransactionJournalTrait.php +++ b/app/Support/Models/TransactionJournalTrait.php @@ -15,16 +15,21 @@ namespace FireflyIII\Support\Models; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournalMeta; +use FireflyIII\Models\TransactionType; use FireflyIII\Support\CacheProperties; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Collection; /** * Class TransactionJournalTrait * - * @property int $id - * @method Collection transactions() - * @method bool isWithdrawal() + * @property int $id + * @property Carbon $date + * @property string $transaction_type_type + * @property TransactionType $transactionType * * @package FireflyIII\Support\Models */ @@ -91,6 +96,16 @@ trait TransactionJournalTrait return 0; } + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + abstract public function budgets(): BelongsToMany; + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + abstract public function categories(): BelongsToMany; + /** * @return string */ @@ -180,6 +195,19 @@ trait TransactionJournalTrait return $list; } + /** + * + * @param string $name + * + * @return string + */ + abstract public function getMeta(string $name); + + /** + * @return bool + */ + abstract public function isDeposit(): bool; + /** * @param Builder $query * @param string $table @@ -201,6 +229,29 @@ trait TransactionJournalTrait return false; } + /** + * + * @return bool + */ + abstract public function isOpeningBalance(): bool; + + /** + * + * @return bool + */ + abstract public function isTransfer(): bool; + + /** + * + * @return bool + */ + abstract public function isWithdrawal(): bool; + + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + abstract public function piggyBankEvents(): HasMany; + /** * @return int */ @@ -213,6 +264,31 @@ trait TransactionJournalTrait return 0; } + /** + * @return Transaction + */ + public function positiveTransaction(): Transaction + { + return $this->transactions()->where('amount', '>', 0)->first(); + } + + /** + * Save the model to the database. + * + * @param array $options + * + * @return bool + */ + abstract public function save(array $options = []): bool; + + /** + * @param string $name + * @param $value + * + * @return TransactionJournalMeta + */ + abstract public function setMeta(string $name, $value): TransactionJournalMeta; + /** * @return Collection */ @@ -273,4 +349,9 @@ trait TransactionJournalTrait return $typeStr; } + + /** + * @return HasMany + */ + abstract public function transactions(): HasMany; } diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index dc28a613db..464fbed037 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -96,7 +96,7 @@ class Navigation // if the range is custom, the end of the period // is another X days (x is the difference between start) // and end added to $theCurrentEnd - if ($repeatFreq == 'custom') { + if ($repeatFreq === 'custom') { /** @var Carbon $tStart */ $tStart = session('start', Carbon::now()->startOfMonth()); /** @var Carbon $tEnd */ @@ -393,7 +393,7 @@ class Navigation return $date; } - if ($repeatFreq == 'half-year' || $repeatFreq == '6M') { + if ($repeatFreq === 'half-year' || $repeatFreq === '6M') { $month = $date->month; $date->startOfYear(); if ($month >= 7) { @@ -496,7 +496,7 @@ class Navigation return $end; } - if ($range == '6M') { + if ($range === '6M') { if ($start->month >= 7) { $end->endOfYear(); @@ -532,7 +532,7 @@ class Navigation return $start; } - if ($range == '6M') { + if ($range === '6M') { if ($start->month >= 7) { $start->startOfYear()->addMonths(6); diff --git a/app/Support/Search/Modifier.php b/app/Support/Search/Modifier.php index 18e94d3aff..acfae12cf6 100644 --- a/app/Support/Search/Modifier.php +++ b/app/Support/Search/Modifier.php @@ -86,17 +86,17 @@ class Modifier case 'date': case 'on': $res = self::sameDate($transaction->date, $modifier['value']); - Log::debug(sprintf('Date is %s? %s', $modifier['value'], var_export($res, true))); + Log::debug(sprintf('Date same as %s? %s', $modifier['value'], var_export($res, true))); break; case 'date_before': case 'before': $res = self::dateBefore($transaction->date, $modifier['value']); - Log::debug(sprintf('Date is %s? %s', $modifier['value'], var_export($res, true))); + Log::debug(sprintf('Date before %s? %s', $modifier['value'], var_export($res, true))); break; case 'date_after': case 'after': $res = self::dateAfter($transaction->date, $modifier['value']); - Log::debug(sprintf('Date is %s? %s', $modifier['value'], var_export($res, true))); + Log::debug(sprintf('Date before %s? %s', $modifier['value'], var_export($res, true))); break; } @@ -208,4 +208,4 @@ class Modifier return self::stringCompare($journalCategory, $search) || self::stringCompare($transactionCategory, $search); } -} \ No newline at end of file +} diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index 4b9ffacb31..ce7b271973 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -17,11 +17,6 @@ namespace FireflyIII\Support\Search; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Helpers\Filter\InternalTransferFilter; -use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; -use FireflyIII\Models\Budget; -use FireflyIII\Models\Category; -use FireflyIII\Models\Tag; use FireflyIII\Models\Transaction; use FireflyIII\User; use Illuminate\Support\Collection; @@ -53,7 +48,7 @@ class Search implements SearchInterface public function __construct() { $this->modifiers = new Collection; - $this->validModifiers = config('firefly.search_modifiers'); + $this->validModifiers = (array)config('firefly.search_modifiers'); } /** @@ -98,112 +93,20 @@ class Search implements SearchInterface } } - /** - * @return Collection - */ - public function searchAccounts(): Collection - { - $words = $this->words; - $accounts = $this->user->accounts() - ->accountTypeIn([AccountType::DEFAULT, AccountType::ASSET, AccountType::EXPENSE, AccountType::REVENUE, AccountType::BENEFICIARY]) - ->get(['accounts.*']); - /** @var Collection $result */ - $result = $accounts->filter( - function (Account $account) use ($words) { - if ($this->strpos_arr(strtolower($account->name), $words)) { - return $account; - } - - return false; - } - ); - - $result = $result->slice(0, $this->limit); - - return $result; - } - - /** - * @return Collection - */ - public function searchBudgets(): Collection - { - /** @var Collection $set */ - $set = auth()->user()->budgets()->get(); - $words = $this->words; - /** @var Collection $result */ - $result = $set->filter( - function (Budget $budget) use ($words) { - if ($this->strpos_arr(strtolower($budget->name), $words)) { - return $budget; - } - - return false; - } - ); - - $result = $result->slice(0, $this->limit); - - return $result; - } - - /** - * @return Collection - */ - public function searchCategories(): Collection - { - $words = $this->words; - $categories = $this->user->categories()->get(); - /** @var Collection $result */ - $result = $categories->filter( - function (Category $category) use ($words) { - if ($this->strpos_arr(strtolower($category->name), $words)) { - return $category; - } - - return false; - } - ); - $result = $result->slice(0, $this->limit); - - return $result; - } - - /** - * @return Collection - */ - public function searchTags(): Collection - { - $words = $this->words; - $tags = $this->user->tags()->get(); - /** @var Collection $result */ - $result = $tags->filter( - function (Tag $tag) use ($words) { - if ($this->strpos_arr(strtolower($tag->tag), $words)) { - return $tag; - } - - return false; - } - ); - $result = $result->slice(0, $this->limit); - - return $result; - } - /** * @return Collection */ public function searchTransactions(): Collection { + Log::debug('Start of searchTransactions()'); $pageSize = 100; $processed = 0; $page = 1; $result = new Collection(); + $startTime = microtime(true); do { /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class); - $collector->setUser($this->user); $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page); if ($this->hasModifiers()) { $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); @@ -247,7 +150,11 @@ class Search implements SearchInterface Log::debug(sprintf('reachedEndOfList: %s', var_export($reachedEndOfList, true))); Log::debug(sprintf('foundEnough: %s', var_export($foundEnough, true))); - } while (!$reachedEndOfList && !$foundEnough); + // break at some point so the script does not crash: + $currentTime = microtime(true) - $startTime; + Log::debug(sprintf('Have been running for %f seconds.', $currentTime)); + + } while (!$reachedEndOfList && !$foundEnough && $currentTime <= 30); $result = $result->slice(0, $this->limit); diff --git a/app/Support/Search/SearchInterface.php b/app/Support/Search/SearchInterface.php index a06ea95d27..d709d02b67 100644 --- a/app/Support/Search/SearchInterface.php +++ b/app/Support/Search/SearchInterface.php @@ -38,25 +38,6 @@ interface SearchInterface */ public function parseQuery(string $query); - /** - * @return Collection - */ - public function searchAccounts(): Collection; - - /** - * @return Collection - */ - public function searchBudgets(): Collection; - - /** - * @return Collection - */ - public function searchCategories(): Collection; - - /** - * @return Collection - */ - public function searchTags(): Collection; /** * @return Collection diff --git a/app/Support/Steam.php b/app/Support/Steam.php index 3b5a27ad0d..7b08463261 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -13,11 +13,14 @@ declare(strict_types=1); namespace FireflyIII\Support; +use Amount as GlobalAmount; use Carbon\Carbon; use Crypt; use DB; use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; +use Illuminate\Contracts\Encryption\DecryptException; +use Illuminate\Support\Collection; /** * Class Steam @@ -45,14 +48,32 @@ class Steam if ($cache->has()) { return $cache->get(); // @codeCoverageIgnore } - - $balance = strval( - $account->transactions()->leftJoin( - 'transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id' - )->where('transaction_journals.date', '<=', $date->format('Y-m-d'))->sum('transactions.amount') + $currencyId = intval($account->getMeta('currency_id')); + // use system default currency: + if ($currencyId === 0) { + $currency = GlobalAmount::getDefaultCurrency(); + $currencyId = $currency->id; + } + // first part: get all balances in own currency: + $nativeBalance = strval( + $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d')) + ->where('transactions.transaction_currency_id', $currencyId) + ->sum('transactions.amount') ); - $virtual = is_null($account->virtual_balance) ? '0' : strval($account->virtual_balance); - $balance = bcadd($balance, $virtual); + + // get all balances in foreign currency: + $foreignBalance = strval( + $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d')) + ->where('transactions.foreign_currency_id', $currencyId) + ->sum('transactions.foreign_amount') + ); + $balance = bcadd($nativeBalance, $foreignBalance); + $virtual = is_null($account->virtual_balance) ? '0' : strval($account->virtual_balance); + $balance = bcadd($balance, $virtual); $cache->store($balance); return $balance; @@ -76,13 +97,26 @@ class Steam if ($cache->has()) { return $cache->get(); // @codeCoverageIgnore } + $currencyId = intval($account->getMeta('currency_id')); - $balance = strval( - $account->transactions()->leftJoin( - 'transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id' - )->where('transaction_journals.date', '<=', $date->format('Y-m-d'))->sum('transactions.amount') + $nativeBalance = strval( + $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d')) + ->where('transactions.transaction_currency_id', $currencyId) + ->sum('transactions.amount') ); + // get all balances in foreign currency: + $foreignBalance = strval( + $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d')) + ->where('transactions.foreign_currency_id', $currencyId) + ->sum('transactions.foreign_amount') + ); + $balance = bcadd($nativeBalance, $foreignBalance); + $cache->store($balance); return $balance; @@ -111,27 +145,55 @@ class Steam return $cache->get(); // @codeCoverageIgnore } - $balances = []; $start->subDay(); $end->addDay(); - $startBalance = $this->balance($account, $start); - $balances[$start->format('Y-m-d')] = $startBalance; + $balances = []; + $formatted = $start->format('Y-m-d'); + $startBalance = $this->balance($account, $start); + $balances[$formatted] = $startBalance; + $currencyId = intval($account->getMeta('currency_id')); $start->addDay(); // query! - $set = $account->transactions() - ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->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')]); + $set = $account->transactions() + ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) + ->groupBy('transaction_journals.date') + ->groupBy('transactions.transaction_currency_id') + ->groupBy('transactions.foreign_currency_id') + ->orderBy('transaction_journals.date', 'ASC') + ->whereNull('transaction_journals.deleted_at') + ->get( + [ + 'transaction_journals.date', + 'transactions.transaction_currency_id', + DB::raw('SUM(transactions.amount) AS modified'), + 'transactions.foreign_currency_id', + DB::raw('SUM(transactions.foreign_amount) AS modified_foreign'), + ] + ); + $currentBalance = $startBalance; + /** @var Transaction $entry */ foreach ($set as $entry) { - $modified = is_null($entry->modified) ? '0' : strval($entry->modified); - $currentBalance = bcadd($currentBalance, $modified); - $balances[$entry->date] = $currentBalance; + // normal amount and foreign amount + $modified = is_null($entry->modified) ? '0' : strval($entry->modified); + $foreignModified = is_null($entry->modified_foreign) ? '0' : strval($entry->modified_foreign); + $amount = '0'; + if ($currencyId === $entry->transaction_currency_id) { + // use normal amount: + $amount = $modified; + } + if ($currencyId === $entry->foreign_currency_id) { + // use normal amount: + $amount = $foreignModified; + } + + $currentBalance = bcadd($currentBalance, $amount); + $carbon = new Carbon($entry->date); + $date = $carbon->format('Y-m-d'); + $balances[$date] = $currentBalance; } $cache->store($balances); @@ -144,14 +206,14 @@ class Steam /** * This method always ignores the virtual balance. * - * @param array $ids - * @param \Carbon\Carbon $date + * @param \Illuminate\Support\Collection $accounts + * @param \Carbon\Carbon $date * * @return array */ - public function balancesById(array $ids, Carbon $date): array + public function balancesByAccounts(Collection $accounts, Carbon $date): array { - + $ids = $accounts->pluck('id')->toArray(); // cache this property. $cache = new CacheProperties; $cache->addProperty($ids); @@ -161,21 +223,13 @@ class Steam return $cache->get(); // @codeCoverageIgnore } - $balances = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->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')]); - + // need to do this per account. $result = []; - foreach ($balances as $entry) { - $accountId = intval($entry->account_id); - $balance = $entry->aggregate; - $result[$accountId] = $balance; + /** @var Account $account */ + foreach ($accounts as $account) { + $result[$account->id] = $this->balance($account, $date); } - $cache->store($result); return $result; @@ -195,6 +249,21 @@ class Steam return $value; } + /** + * @param $value + * + * @return mixed + */ + public function tryDecrypt($value) + { + try { + $value = Crypt::decrypt($value); + } catch (DecryptException $e) { + // do not care. + } + + return $value; + } /** * @param array $accounts @@ -217,6 +286,32 @@ class Steam return $list; } + /** + * @param string $amount + * + * @return string + */ + public function negative(string $amount): string + { + if (bccomp($amount, '0') === 1) { + $amount = bcmul($amount, '-1'); + } + + return $amount; + } + + /** + * @param string $amount + * + * @return string + */ + public function opposite(string $amount): string + { + $amount = bcmul($amount, '-1'); + + return $amount; + } + /** * @param $string * @@ -252,8 +347,6 @@ class Steam } - // parse PHP size: - /** * @param string $amount * diff --git a/app/Support/Twig/Account.php b/app/Support/Twig/Account.php deleted file mode 100644 index d2a5520505..0000000000 --- a/app/Support/Twig/Account.php +++ /dev/null @@ -1,63 +0,0 @@ -formatAmountByAccount(), - ]; - - } - - /** - * Will return "active" when a part of the route matches the argument. - * ie. "accounts" will match "accounts.index". - * - * @return Twig_SimpleFunction - */ - protected function formatAmountByAccount(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'formatAmountByAccount', function (AccountModel $account, string $amount, bool $coloured = true): string { - $currencyId = intval($account->getMeta('currency_id')); - if ($currencyId === 0) { - // Format using default currency: - return AmountFacade::format($amount, $coloured); - } - $currency = TransactionCurrency::find($currencyId); - - return AmountFacade::formatAnything($currency, $amount, $coloured); - }, ['is_safe' => ['html']] - ); - } - - -} \ No newline at end of file diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php new file mode 100644 index 0000000000..55e5c28942 --- /dev/null +++ b/app/Support/Twig/AmountFormat.php @@ -0,0 +1,268 @@ +formatAmount(), + $this->formatAmountPlain(), + ]; + + } + + /** + * {@inheritDoc} + */ + public function getFunctions(): array + { + return [ + $this->formatAmountByAccount(), + $this->transactionAmount(), + $this->journalAmount(), + $this->formatDestinationAfter(), + $this->formatDestinationBefore(), + $this->formatSourceAfter(), + $this->formatSourceBefore(), + $this->formatAmountByCurrency(), + ]; + + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName(): string + { + return 'FireflyIII\Support\Twig\AmountFormat'; + } + + /** + * + * @return Twig_SimpleFilter + */ + protected function formatAmount(): Twig_SimpleFilter + { + return new Twig_SimpleFilter( + 'formatAmount', function (string $string): string { + + $currency = app('amount')->getDefaultCurrency(); + + return app('amount')->formatAnything($currency, $string, true); + }, ['is_safe' => ['html']] + ); + } + + /** + * Will format the amount by the currency related to the given account. + * + * @return Twig_SimpleFunction + */ + protected function formatAmountByAccount(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'formatAmountByAccount', function (AccountModel $account, string $amount, bool $coloured = true): string { + $currencyId = intval($account->getMeta('currency_id')); + + if ($currencyId !== 0) { + $currency = TransactionCurrency::find($currencyId); + + return app('amount')->formatAnything($currency, $amount, $coloured); + } + $currency = app('amount')->getDefaultCurrency(); + + return app('amount')->formatAnything($currency, $amount, $coloured); + + + }, ['is_safe' => ['html']] + ); + } + + /** + * Will format the amount by the currency related to the given account. + * + * @return Twig_SimpleFunction + */ + protected function formatAmountByCurrency(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'formatAmountByCurrency', function (TransactionCurrency $currency, string $amount, bool $coloured = true): string { + + return app('amount')->formatAnything($currency, $amount, $coloured); + + + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFilter + */ + protected function formatAmountPlain(): Twig_SimpleFilter + { + return new Twig_SimpleFilter( + 'formatAmountPlain', function (string $string): string { + + $currency = app('amount')->getDefaultCurrency(); + + return app('amount')->formatAnything($currency, $string, false); + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFunction + */ + protected function formatDestinationAfter(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'formatDestinationAfter', function (array $transaction): string { + + // build fake currency for main amount. + $format = new TransactionCurrency; + $format->decimal_places = $transaction['transaction_currency_dp']; + $format->symbol = $transaction['transaction_currency_symbol']; + $string = app('amount')->formatAnything($format, $transaction['destination_account_after'], true); + + // also append foreign amount for clarity: + if (!is_null($transaction['foreign_destination_amount'])) { + // build fake currency for foreign amount + $format = new TransactionCurrency; + $format->decimal_places = $transaction['foreign_currency_dp']; + $format->symbol = $transaction['foreign_currency_symbol']; + $string .= ' (' . app('amount')->formatAnything($format, $transaction['foreign_destination_amount'], true) . ')'; + } + + + return $string; + + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFunction + */ + protected function formatDestinationBefore(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'formatDestinationBefore', function (array $transaction): string { + + // build fake currency for main amount. + $format = new TransactionCurrency; + $format->decimal_places = $transaction['transaction_currency_dp']; + $format->symbol = $transaction['transaction_currency_symbol']; + + return app('amount')->formatAnything($format, $transaction['destination_account_before'], true); + + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFunction + */ + protected function formatSourceAfter(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'formatSourceAfter', function (array $transaction): string { + + // build fake currency for main amount. + $format = new TransactionCurrency; + $format->decimal_places = $transaction['transaction_currency_dp']; + $format->symbol = $transaction['transaction_currency_symbol']; + $string = app('amount')->formatAnything($format, $transaction['source_account_after'], true); + + // also append foreign amount for clarity: + if (!is_null($transaction['foreign_source_amount'])) { + // build fake currency for foreign amount + $format = new TransactionCurrency; + $format->decimal_places = $transaction['foreign_currency_dp']; + $format->symbol = $transaction['foreign_currency_symbol']; + $string .= ' (' . app('amount')->formatAnything($format, $transaction['foreign_source_amount'], true) . ')'; + } + + + return $string; + + + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFunction + */ + protected function formatSourceBefore(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'formatSourceBefore', function (array $transaction): string { + + // build fake currency for main amount. + $format = new TransactionCurrency; + $format->decimal_places = $transaction['transaction_currency_dp']; + $format->symbol = $transaction['transaction_currency_symbol']; + + return app('amount')->formatAnything($format, $transaction['source_account_before'], true); + + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFunction + */ + protected function journalAmount(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'journalAmount', function (TransactionJournal $journal): string { + + return app('amount')->journalAmount($journal, true); + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFunction + */ + protected function transactionAmount(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'transactionAmount', function (TransactionModel $transaction): string { + + return app('amount')->transactionAmount($transaction, true); + }, ['is_safe' => ['html']] + ); + } + +} diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php index 5db0ba95f6..09183c84e5 100644 --- a/app/Support/Twig/General.php +++ b/app/Support/Twig/General.php @@ -38,9 +38,6 @@ class General extends Twig_Extension public function getFilters(): array { return [ - $this->formatAmount(), - $this->formatAmountPlain(), - $this->formatJournal(), $this->balance(), $this->formatFilesize(), $this->mimeIcon(), @@ -113,7 +110,7 @@ class General extends Twig_Extension $what = $args[2]; // name of the route. $activeWhat = $context['what'] ?? false; - if ($what == $activeWhat && !(strpos(Route::getCurrentRoute()->getName(), $route) === false)) { + if ($what === $activeWhat && !(strpos(Route::getCurrentRoute()->getName(), $route) === false)) { return 'active'; } @@ -135,7 +132,7 @@ class General extends Twig_Extension $args = func_get_args(); $route = $args[0]; // name of the route. - if (Route::getCurrentRoute()->getName() == $route) { + if (Route::getCurrentRoute()->getName() === $route) { return 'active'; } @@ -173,33 +170,6 @@ class General extends Twig_Extension ); } - /** - * - * @return Twig_SimpleFilter - */ - protected function formatAmount(): Twig_SimpleFilter - { - return new Twig_SimpleFilter( - 'formatAmount', function (string $string): string { - - return app('amount')->format($string); - }, ['is_safe' => ['html']] - ); - } - - /** - * @return Twig_SimpleFilter - */ - protected function formatAmountPlain(): Twig_SimpleFilter - { - return new Twig_SimpleFilter( - 'formatAmountPlain', function (string $string): string { - - return app('amount')->format($string, false); - }, ['is_safe' => ['html']] - ); - } - /** * @return Twig_SimpleFilter */ @@ -223,17 +193,6 @@ class General extends Twig_Extension ); } - /** - * @return Twig_SimpleFilter - */ - protected function formatJournal(): Twig_SimpleFilter - { - return new Twig_SimpleFilter( - 'formatJournal', function (TransactionJournal $journal): string { - return app('amount')->formatJournal($journal); - }, ['is_safe' => ['html']] - ); - } /** * @return Twig_SimpleFunction diff --git a/app/Support/Twig/Journal.php b/app/Support/Twig/Journal.php index d3c0f314e5..82a5553c4d 100644 --- a/app/Support/Twig/Journal.php +++ b/app/Support/Twig/Journal.php @@ -51,7 +51,7 @@ class Journal extends Twig_Extension $array = []; /** @var Account $entry */ foreach ($list as $entry) { - if ($entry->accountType->type == AccountType::CASH) { + if ($entry->accountType->type === AccountType::CASH) { $array[] = '(cash)'; continue; } @@ -123,7 +123,7 @@ class Journal extends Twig_Extension $array = []; /** @var Account $entry */ foreach ($list as $entry) { - if ($entry->accountType->type == 'Cash account') { + if ($entry->accountType->type === AccountType::CASH) { $array[] = '(cash)'; continue; } diff --git a/app/Support/Twig/Rule.php b/app/Support/Twig/Rule.php index 2503cec452..9924e6f7a0 100644 --- a/app/Support/Twig/Rule.php +++ b/app/Support/Twig/Rule.php @@ -70,7 +70,7 @@ class Rule extends Twig_Extension $ruleTriggers = array_keys(Config::get('firefly.rule-triggers')); $possibleTriggers = []; foreach ($ruleTriggers as $key) { - if ($key != 'user_action') { + if ($key !== 'user_action') { $possibleTriggers[$key] = trans('firefly.rule_trigger_' . $key . '_choice'); } } diff --git a/app/Support/Twig/Transaction.php b/app/Support/Twig/Transaction.php index d4b8e84850..3774940216 100644 --- a/app/Support/Twig/Transaction.php +++ b/app/Support/Twig/Transaction.php @@ -13,10 +13,8 @@ declare(strict_types=1); namespace FireflyIII\Support\Twig; -use Amount; use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction as TransactionModel; -use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; use Steam; use Twig_Extension; @@ -30,49 +28,6 @@ use Twig_SimpleFunction; */ class Transaction extends Twig_Extension { - - /** - * @return Twig_SimpleFunction - */ - public function formatAnything(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'formatAnything', function (TransactionCurrency $currency, string $amount): string { - - return Amount::formatAnything($currency, $amount, true); - - }, ['is_safe' => ['html']] - ); - } - - /** - * @return Twig_SimpleFunction - */ - public function formatAnythingPlain(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'formatAnythingPlain', function (TransactionCurrency $currency, string $amount): string { - - return Amount::formatAnything($currency, $amount, false); - - }, ['is_safe' => ['html']] - ); - } - - /** - * @return Twig_SimpleFunction - */ - public function formatByCode(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'formatByCode', function (string $currencyCode, string $amount): string { - - return Amount::formatByCode($currencyCode, $amount, true); - - }, ['is_safe' => ['html']] - ); - } - /** * @return array */ @@ -91,17 +46,13 @@ class Transaction extends Twig_Extension public function getFunctions(): array { $functions = [ - $this->formatAnything(), - $this->formatAnythingPlain(), $this->transactionSourceAccount(), $this->transactionDestinationAccount(), - $this->optionalJournalAmount(), $this->transactionBudgets(), $this->transactionIdBudgets(), $this->transactionCategories(), $this->transactionIdCategories(), $this->splitJournalIndicator(), - $this->formatByCode(), ]; return $functions; @@ -117,33 +68,6 @@ class Transaction extends Twig_Extension return 'transaction'; } - /** - * @return Twig_SimpleFunction - */ - public function optionalJournalAmount(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'optionalJournalAmount', function (int $journalId, string $transactionAmount, string $code, string $type): string { - // get amount of journal: - $amount = strval(TransactionModel::where('transaction_journal_id', $journalId)->whereNull('deleted_at')->where('amount', '<', 0)->sum('amount')); - // display deposit and transfer positive - if ($type === TransactionType::DEPOSIT || $type === TransactionType::TRANSFER) { - $amount = bcmul($amount, '-1'); - } - - // not equal to transaction amount? - if (bccomp($amount, $transactionAmount) !== 0 && bccomp($amount, bcmul($transactionAmount, '-1')) !== 0) { - //$currency = - return sprintf(' (%s)', Amount::formatByCode($code, $amount, true)); - } - - return ''; - - - }, ['is_safe' => ['html']] - ); - } - /** * @return Twig_SimpleFunction */ diff --git a/app/User.php b/app/User.php index f5ee28f920..da5e565d38 100644 --- a/app/User.php +++ b/app/User.php @@ -149,7 +149,7 @@ class User extends Authenticatable { foreach ($this->roles as $role) { - if ($role->name == $name) { + if ($role->name === $name) { return true; } } @@ -214,9 +214,9 @@ class User extends Authenticatable */ public function sendPasswordResetNotification($token) { - $ip = Request::ip(); + $ipAddress = Request::ip(); - event(new RequestedNewPassword($this, $token, $ip)); + event(new RequestedNewPassword($this, $token, $ipAddress)); } /** diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index cf4b60c3f9..e08c95a22a 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -211,7 +211,7 @@ class FireflyValidator extends Validator // count budgets, should have at least one $count = $budgets->filter( function (Budget $budget) use ($value) { - return $budget->name == $value; + return $budget->name === $value; } )->count(); @@ -329,7 +329,7 @@ class FireflyValidator extends Validator /** @var AccountMeta $entry */ foreach ($set as $entry) { - if ($entry->data == $value) { + if ($entry->data === $value) { return false; } @@ -398,7 +398,7 @@ class FireflyValidator extends Validator /** @var PiggyBank $entry */ foreach ($set as $entry) { $fieldValue = $this->tryDecrypt($entry->name); - if ($fieldValue == $value) { + if ($fieldValue === $value) { return false; } } @@ -460,7 +460,7 @@ class FireflyValidator extends Validator $set = $user->accounts()->where('account_type_id', $type->id)->get(); /** @var Account $entry */ foreach ($set as $entry) { - if ($entry->name == $value) { + if ($entry->name === $value) { return false; } } @@ -486,7 +486,7 @@ class FireflyValidator extends Validator $set = auth()->user()->accounts()->where('account_type_id', $type->id)->where('id', '!=', $ignore)->get(); /** @var Account $entry */ foreach ($set as $entry) { - if ($entry->name == $value) { + if ($entry->name === $value) { return false; } } @@ -510,7 +510,7 @@ class FireflyValidator extends Validator $set = auth()->user()->accounts()->where('account_type_id', $type->id)->where('id', '!=', $ignore)->get(); /** @var Account $entry */ foreach ($set as $entry) { - if ($entry->name == $value) { + if ($entry->name === $value) { return false; } } @@ -534,7 +534,7 @@ class FireflyValidator extends Validator $set = auth()->user()->accounts()->where('account_type_id', $type->id)->where('id', '!=', $ignore)->get(); /** @var Account $entry */ foreach ($set as $entry) { - if ($entry->name == $value) { + if ($entry->name === $value) { return false; } } diff --git a/composer.lock b/composer.lock index 5363e7571a..1118947c51 100644 --- a/composer.lock +++ b/composer.lock @@ -104,16 +104,16 @@ }, { "name": "davejamesmiller/laravel-breadcrumbs", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/davejamesmiller/laravel-breadcrumbs.git", - "reference": "6ca5a600003ecb52a5b5af14dad82033058604e1" + "reference": "0b0f4792dee645b0f084164aa17d4320e4bb734f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/davejamesmiller/laravel-breadcrumbs/zipball/6ca5a600003ecb52a5b5af14dad82033058604e1", - "reference": "6ca5a600003ecb52a5b5af14dad82033058604e1", + "url": "https://api.github.com/repos/davejamesmiller/laravel-breadcrumbs/zipball/0b0f4792dee645b0f084164aa17d4320e4bb734f", + "reference": "0b0f4792dee645b0f084164aa17d4320e4bb734f", "shasum": "" }, "require": { @@ -124,8 +124,7 @@ "require-dev": { "mockery/mockery": "0.9.*", "orchestra/testbench": "3.2.*|3.3.*", - "phpunit/phpunit": "4.*", - "satooshi/php-coveralls": "0.6.*" + "phpunit/phpunit": "4.*" }, "type": "library", "autoload": { @@ -144,12 +143,12 @@ "homepage": "https://davejamesmiller.com/" } ], - "description": "A simple Laravel-style way to create breadcrumbs in Laravel 4+.", + "description": "A simple Laravel-style way to create breadcrumbs in Laravel.", "homepage": "https://laravel-breadcrumbs.readthedocs.io/", "keywords": [ "laravel" ], - "time": "2017-01-30T21:16:53+00:00" + "time": "2017-06-24T11:10:49+00:00" }, { "name": "doctrine/annotations", @@ -623,16 +622,16 @@ }, { "name": "erusev/parsedown", - "version": "1.6.2", + "version": "1.6.3", "source": { "type": "git", "url": "https://github.com/erusev/parsedown.git", - "reference": "1bf24f7334fe16c88bf9d467863309ceaf285b01" + "reference": "728952b90a333b5c6f77f06ea9422b94b585878d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/1bf24f7334fe16c88bf9d467863309ceaf285b01", - "reference": "1bf24f7334fe16c88bf9d467863309ceaf285b01", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/728952b90a333b5c6f77f06ea9422b94b585878d", + "reference": "728952b90a333b5c6f77f06ea9422b94b585878d", "shasum": "" }, "require": { @@ -661,20 +660,20 @@ "markdown", "parser" ], - "time": "2017-03-29T16:04:15+00:00" + "time": "2017-05-14T14:47:48+00:00" }, { "name": "laravel/framework", - "version": "v5.4.21", + "version": "v5.4.28", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "2ed668f96d1a6ca42f50d5c87ee9ceecfc0a6eee" + "reference": "442511fc62121085d184355e4f964c88942bbecb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/2ed668f96d1a6ca42f50d5c87ee9ceecfc0a6eee", - "reference": "2ed668f96d1a6ca42f50d5c87ee9ceecfc0a6eee", + "url": "https://api.github.com/repos/laravel/framework/zipball/442511fc62121085d184355e4f964c88942bbecb", + "reference": "442511fc62121085d184355e4f964c88942bbecb", "shasum": "" }, "require": { @@ -790,20 +789,20 @@ "framework", "laravel" ], - "time": "2017-04-28T15:40:01+00:00" + "time": "2017-06-30T13:43:07+00:00" }, { "name": "laravelcollective/html", - "version": "v5.4.1", + "version": "v5.4.8", "source": { "type": "git", "url": "https://github.com/LaravelCollective/html.git", - "reference": "7570f25d58a00fd6909c0563808590f9cdb14d47" + "reference": "9b8f51e7a2368911c896f5d42757886bae0717b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/LaravelCollective/html/zipball/7570f25d58a00fd6909c0563808590f9cdb14d47", - "reference": "7570f25d58a00fd6909c0563808590f9cdb14d47", + "url": "https://api.github.com/repos/LaravelCollective/html/zipball/9b8f51e7a2368911c896f5d42757886bae0717b5", + "reference": "9b8f51e7a2368911c896f5d42757886bae0717b5", "shasum": "" }, "require": { @@ -844,20 +843,20 @@ ], "description": "HTML and Form Builders for the Laravel Framework", "homepage": "http://laravelcollective.com", - "time": "2017-01-26T19:27:05+00:00" + "time": "2017-05-22T06:35:07+00:00" }, { "name": "league/commonmark", - "version": "0.15.3", + "version": "0.15.4", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "c8b43ee5821362216f8e9ac684f0f59de164edcc" + "reference": "c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c8b43ee5821362216f8e9ac684f0f59de164edcc", - "reference": "c8b43ee5821362216f8e9ac684f0f59de164edcc", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c", + "reference": "c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c", "shasum": "" }, "require": { @@ -913,7 +912,7 @@ "markdown", "parser" ], - "time": "2016-12-19T00:11:43+00:00" + "time": "2017-05-09T12:47:53+00:00" }, { "name": "league/csv", @@ -1057,16 +1056,16 @@ }, { "name": "monolog/monolog", - "version": "1.22.1", + "version": "1.23.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "1e044bc4b34e91743943479f1be7a1d5eb93add0" + "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1e044bc4b34e91743943479f1be7a1d5eb93add0", - "reference": "1e044bc4b34e91743943479f1be7a1d5eb93add0", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", "shasum": "" }, "require": { @@ -1087,7 +1086,7 @@ "phpunit/phpunit-mock-objects": "2.3.0", "ruflin/elastica": ">=0.90 <3.0", "sentry/sentry": "^0.13", - "swiftmailer/swiftmailer": "~5.3" + "swiftmailer/swiftmailer": "^5.3|^6.0" }, "suggest": { "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", @@ -1131,7 +1130,7 @@ "logging", "psr-3" ], - "time": "2017-03-13T07:08:03+00:00" + "time": "2017-06-19T01:22:40+00:00" }, { "name": "mtdowling/cron-expression", @@ -1583,16 +1582,16 @@ }, { "name": "swiftmailer/swiftmailer", - "version": "v5.4.7", + "version": "v5.4.8", "source": { "type": "git", "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4" + "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4", - "reference": "56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/9a06dc570a0367850280eefd3f1dc2da45aef517", + "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517", "shasum": "" }, "require": { @@ -1633,20 +1632,20 @@ "mail", "mailer" ], - "time": "2017-04-20T17:32:18+00:00" + "time": "2017-05-01T15:54:03+00:00" }, { "name": "symfony/console", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38" + "reference": "a97e45d98c59510f085fa05225a1acb74dfe0546" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38", - "reference": "a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38", + "url": "https://api.github.com/repos/symfony/console/zipball/a97e45d98c59510f085fa05225a1acb74dfe0546", + "reference": "a97e45d98c59510f085fa05225a1acb74dfe0546", "shasum": "" }, "require": { @@ -1654,10 +1653,16 @@ "symfony/debug": "~2.8|~3.0", "symfony/polyfill-mbstring": "~1.0" }, + "conflict": { + "symfony/dependency-injection": "<3.3" + }, "require-dev": { "psr/log": "~1.0", + "symfony/config": "~3.3", + "symfony/dependency-injection": "~3.3", "symfony/event-dispatcher": "~2.8|~3.0", "symfony/filesystem": "~2.8|~3.0", + "symfony/http-kernel": "~2.8|~3.0", "symfony/process": "~2.8|~3.0" }, "suggest": { @@ -1669,7 +1674,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -1696,7 +1701,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-04-26T01:39:17+00:00" + "time": "2017-07-03T13:19:36+00:00" }, { "name": "symfony/css-selector", @@ -1753,16 +1758,16 @@ }, { "name": "symfony/debug", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "fd6eeee656a5a7b384d56f1072243fe1c0e81686" + "reference": "63b85a968486d95ff9542228dc2e4247f16f9743" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/fd6eeee656a5a7b384d56f1072243fe1c0e81686", - "reference": "fd6eeee656a5a7b384d56f1072243fe1c0e81686", + "url": "https://api.github.com/repos/symfony/debug/zipball/63b85a968486d95ff9542228dc2e4247f16f9743", + "reference": "63b85a968486d95ff9542228dc2e4247f16f9743", "shasum": "" }, "require": { @@ -1773,13 +1778,12 @@ "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" }, "require-dev": { - "symfony/class-loader": "~2.8|~3.0", "symfony/http-kernel": "~2.8|~3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -1806,20 +1810,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-04-19T20:17:50+00:00" + "time": "2017-07-05T13:02:37+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.20", + "version": "v2.8.24", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "7fc8e2b4118ff316550596357325dfd92a51f531" + "reference": "1377400fd641d7d1935981546aaef780ecd5bf6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7fc8e2b4118ff316550596357325dfd92a51f531", - "reference": "7fc8e2b4118ff316550596357325dfd92a51f531", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/1377400fd641d7d1935981546aaef780ecd5bf6d", + "reference": "1377400fd641d7d1935981546aaef780ecd5bf6d", "shasum": "" }, "require": { @@ -1866,20 +1870,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-04-26T16:56:54+00:00" + "time": "2017-06-02T07:47:27+00:00" }, { "name": "symfony/finder", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930" + "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9cf076f8f492f4b1ffac40aae9c2d287b4ca6930", - "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930", + "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4", + "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4", "shasum": "" }, "require": { @@ -1888,7 +1892,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -1915,20 +1919,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-06-01T21:01:25+00:00" }, { "name": "symfony/http-foundation", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "9de6add7f731e5af7f5b2e9c0da365e43383ebef" + "reference": "f347a5f561b03db95ed666959db42bbbf429b7e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9de6add7f731e5af7f5b2e9c0da365e43383ebef", - "reference": "9de6add7f731e5af7f5b2e9c0da365e43383ebef", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f347a5f561b03db95ed666959db42bbbf429b7e5", + "reference": "f347a5f561b03db95ed666959db42bbbf429b7e5", "shasum": "" }, "require": { @@ -1941,7 +1945,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -1968,20 +1972,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-05-01T14:55:58+00:00" + "time": "2017-06-24T09:29:48+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.2.8", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "46e8b209abab55c072c47d72d5cd1d62c0585e05" + "reference": "4ad34a0d20a5848c0fcbf6ff6a2ff1cd9cf4b9ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/46e8b209abab55c072c47d72d5cd1d62c0585e05", - "reference": "46e8b209abab55c072c47d72d5cd1d62c0585e05", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4ad34a0d20a5848c0fcbf6ff6a2ff1cd9cf4b9ed", + "reference": "4ad34a0d20a5848c0fcbf6ff6a2ff1cd9cf4b9ed", "shasum": "" }, "require": { @@ -1989,18 +1993,21 @@ "psr/log": "~1.0", "symfony/debug": "~2.8|~3.0", "symfony/event-dispatcher": "~2.8|~3.0", - "symfony/http-foundation": "~2.8.13|~3.1.6|~3.2" + "symfony/http-foundation": "~3.3" }, "conflict": { - "symfony/config": "<2.8" + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.3", + "symfony/var-dumper": "<3.3" }, "require-dev": { + "psr/cache": "~1.0", "symfony/browser-kit": "~2.8|~3.0", "symfony/class-loader": "~2.8|~3.0", "symfony/config": "~2.8|~3.0", "symfony/console": "~2.8|~3.0", "symfony/css-selector": "~2.8|~3.0", - "symfony/dependency-injection": "~2.8|~3.0", + "symfony/dependency-injection": "~3.3", "symfony/dom-crawler": "~2.8|~3.0", "symfony/expression-language": "~2.8|~3.0", "symfony/finder": "~2.8|~3.0", @@ -2009,7 +2016,7 @@ "symfony/stopwatch": "~2.8|~3.0", "symfony/templating": "~2.8|~3.0", "symfony/translation": "~2.8|~3.0", - "symfony/var-dumper": "~3.2" + "symfony/var-dumper": "~3.3" }, "suggest": { "symfony/browser-kit": "", @@ -2023,7 +2030,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2050,20 +2057,20 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-05-01T17:46:48+00:00" + "time": "2017-05-29T21:02:12+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + "reference": "f29dca382a6485c3cbe6379f0c61230167681937" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937", + "reference": "f29dca382a6485c3cbe6379f0c61230167681937", "shasum": "" }, "require": { @@ -2075,7 +2082,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -2109,20 +2116,20 @@ "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2017-06-09T14:24:12+00:00" }, { "name": "symfony/polyfill-php56", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php56.git", - "reference": "1dd42b9b89556f18092f3d1ada22cb05ac85383c" + "reference": "bc0b7d6cb36b10cfabb170a3e359944a95174929" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/1dd42b9b89556f18092f3d1ada22cb05ac85383c", - "reference": "1dd42b9b89556f18092f3d1ada22cb05ac85383c", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/bc0b7d6cb36b10cfabb170a3e359944a95174929", + "reference": "bc0b7d6cb36b10cfabb170a3e359944a95174929", "shasum": "" }, "require": { @@ -2132,7 +2139,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -2165,20 +2172,20 @@ "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2017-06-09T08:25:21+00:00" }, { "name": "symfony/polyfill-util", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-util.git", - "reference": "746bce0fca664ac0a575e465f65c6643faddf7fb" + "reference": "ebccbde4aad410f6438d86d7d261c6b4d2b9a51d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/746bce0fca664ac0a575e465f65c6643faddf7fb", - "reference": "746bce0fca664ac0a575e465f65c6643faddf7fb", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/ebccbde4aad410f6438d86d7d261c6b4d2b9a51d", + "reference": "ebccbde4aad410f6438d86d7d261c6b4d2b9a51d", "shasum": "" }, "require": { @@ -2187,7 +2194,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -2217,20 +2224,20 @@ "polyfill", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2017-06-09T08:25:21+00:00" }, { "name": "symfony/process", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0" + "reference": "5ab8949b682b1bf9d4511a228b5e045c96758c30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", - "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", + "url": "https://api.github.com/repos/symfony/process/zipball/5ab8949b682b1bf9d4511a228b5e045c96758c30", + "reference": "5ab8949b682b1bf9d4511a228b5e045c96758c30", "shasum": "" }, "require": { @@ -2239,7 +2246,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2266,36 +2273,39 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-07-03T08:12:02+00:00" }, { "name": "symfony/routing", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "5029745d6d463585e8b487dbc83d6333f408853a" + "reference": "dc70bbd0ca7b19259f63cdacc8af370bc32a4728" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/5029745d6d463585e8b487dbc83d6333f408853a", - "reference": "5029745d6d463585e8b487dbc83d6333f408853a", + "url": "https://api.github.com/repos/symfony/routing/zipball/dc70bbd0ca7b19259f63cdacc8af370bc32a4728", + "reference": "dc70bbd0ca7b19259f63cdacc8af370bc32a4728", "shasum": "" }, "require": { "php": ">=5.5.9" }, "conflict": { - "symfony/config": "<2.8" + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.3", + "symfony/yaml": "<3.3" }, "require-dev": { "doctrine/annotations": "~1.0", "doctrine/common": "~2.2", "psr/log": "~1.0", "symfony/config": "~2.8|~3.0", + "symfony/dependency-injection": "~3.3", "symfony/expression-language": "~2.8|~3.0", "symfony/http-foundation": "~2.8|~3.0", - "symfony/yaml": "~2.8|~3.0" + "symfony/yaml": "~3.3" }, "suggest": { "doctrine/annotations": "For using the annotation loader", @@ -2308,7 +2318,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2341,20 +2351,20 @@ "uri", "url" ], - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-06-24T09:29:48+00:00" }, { "name": "symfony/translation", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "f4a04d2df710f81515df576b2de06bdeee518b83" + "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/f4a04d2df710f81515df576b2de06bdeee518b83", - "reference": "f4a04d2df710f81515df576b2de06bdeee518b83", + "url": "https://api.github.com/repos/symfony/translation/zipball/35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3", + "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3", "shasum": "" }, "require": { @@ -2362,13 +2372,14 @@ "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/config": "<2.8" + "symfony/config": "<2.8", + "symfony/yaml": "<3.3" }, "require-dev": { "psr/log": "~1.0", "symfony/config": "~2.8|~3.0", "symfony/intl": "^2.8.18|^3.2.5", - "symfony/yaml": "~2.8|~3.0" + "symfony/yaml": "~3.3" }, "suggest": { "psr/log": "To use logging capability in translator", @@ -2378,7 +2389,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2405,20 +2416,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-06-24T16:45:30+00:00" }, { "name": "symfony/var-dumper", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8" + "reference": "9ee920bba1d2ce877496dcafca7cbffff4dbe08a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/fa47963ac7979ddbd42b2d646d1b056bddbf7bb8", - "reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9ee920bba1d2ce877496dcafca7cbffff4dbe08a", + "reference": "9ee920bba1d2ce877496dcafca7cbffff4dbe08a", "shasum": "" }, "require": { @@ -2430,7 +2441,7 @@ }, "require-dev": { "ext-iconv": "*", - "twig/twig": "~1.20|~2.0" + "twig/twig": "~1.34|~2.4" }, "suggest": { "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", @@ -2439,7 +2450,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2473,7 +2484,7 @@ "debug", "dump" ], - "time": "2017-05-01T14:55:58+00:00" + "time": "2017-07-05T13:02:37+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -2687,20 +2698,20 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v2.3.2", + "version": "v2.4.1", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "24e4f0261e352d3fd86d0447791b56ae49398674" + "reference": "af98b3a4ccac9364f2145fae974ff3392ec402b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/24e4f0261e352d3fd86d0447791b56ae49398674", - "reference": "24e4f0261e352d3fd86d0447791b56ae49398674", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/af98b3a4ccac9364f2145fae974ff3392ec402b1", + "reference": "af98b3a4ccac9364f2145fae974ff3392ec402b1", "shasum": "" }, "require": { - "illuminate/support": "5.1.*|5.2.*|5.3.*|5.4.*", + "illuminate/support": "5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", "maximebf/debugbar": "~1.13.0", "php": ">=5.5.9", "symfony/finder": "~2.7|~3.0" @@ -2708,7 +2719,15 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.4-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ], + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facade" + } } }, "autoload": { @@ -2737,32 +2756,34 @@ "profiler", "webprofiler" ], - "time": "2017-01-19T08:19:49+00:00" + "time": "2017-06-14T07:44:44+00:00" }, { "name": "barryvdh/laravel-ide-helper", - "version": "v2.3.2", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "e82de98cef0d6597b1b686be0b5813a3a4bb53c5" + "reference": "87a02ff574f722c685e011f76692ab869ab64f6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/e82de98cef0d6597b1b686be0b5813a3a4bb53c5", - "reference": "e82de98cef0d6597b1b686be0b5813a3a4bb53c5", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/87a02ff574f722c685e011f76692ab869ab64f6c", + "reference": "87a02ff574f722c685e011f76692ab869ab64f6c", "shasum": "" }, "require": { "barryvdh/reflection-docblock": "^2.0.4", - "illuminate/console": "^5.0,<5.5", - "illuminate/filesystem": "^5.0,<5.5", - "illuminate/support": "^5.0,<5.5", + "illuminate/console": "^5.0,<5.6", + "illuminate/filesystem": "^5.0,<5.6", + "illuminate/support": "^5.0,<5.6", "php": ">=5.4.0", "symfony/class-loader": "^2.3|^3.0" }, "require-dev": { "doctrine/dbal": "~2.3", + "illuminate/config": "^5.0,<5.6", + "illuminate/view": "^5.0,<5.6", "phpunit/phpunit": "4.*", "scrutinizer/ocular": "~1.1", "squizlabs/php_codesniffer": "~2.3" @@ -2774,6 +2795,11 @@ "extra": { "branch-alias": { "dev-master": "2.3-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider" + ] } }, "autoload": { @@ -2803,7 +2829,7 @@ "phpstorm", "sublime" ], - "time": "2017-02-22T12:27:33+00:00" + "time": "2017-06-16T14:08:59+00:00" }, { "name": "barryvdh/reflection-docblock", @@ -3725,16 +3751,16 @@ }, { "name": "phpunit/phpunit", - "version": "5.7.19", + "version": "5.7.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1" + "reference": "3b91adfb64264ddec5a2dee9851f354aa66327db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/69c4f49ff376af2692bad9cebd883d17ebaa98a1", - "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3b91adfb64264ddec5a2dee9851f354aa66327db", + "reference": "3b91adfb64264ddec5a2dee9851f354aa66327db", "shasum": "" }, "require": { @@ -3752,7 +3778,7 @@ "phpunit/php-timer": "^1.0.6", "phpunit/phpunit-mock-objects": "^3.2", "sebastian/comparator": "^1.2.4", - "sebastian/diff": "~1.2", + "sebastian/diff": "^1.4.3", "sebastian/environment": "^1.3.4 || ^2.0", "sebastian/exporter": "~2.0", "sebastian/global-state": "^1.1", @@ -3803,20 +3829,20 @@ "testing", "xunit" ], - "time": "2017-04-03T02:22:27+00:00" + "time": "2017-06-21T08:11:54+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", "shasum": "" }, "require": { @@ -3862,7 +3888,7 @@ "mock", "xunit" ], - "time": "2016-12-08T20:27:08+00:00" + "time": "2017-06-30T09:13:00+00:00" }, { "name": "satooshi/php-coveralls", @@ -4033,23 +4059,23 @@ }, { "name": "sebastian/diff", - "version": "1.4.1", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^5.3.3 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.8" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" }, "type": "library", "extra": { @@ -4081,7 +4107,7 @@ "keywords": [ "diff" ], - "time": "2015-12-08T07:14:41+00:00" + "time": "2017-05-22T07:24:03+00:00" }, { "name": "sebastian/environment", @@ -4437,16 +4463,16 @@ }, { "name": "symfony/class-loader", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "fc4c04bfd17130a9dccfded9578353f311967da7" + "reference": "386a294d621576302e7cc36965d6ed53b8c73c4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/fc4c04bfd17130a9dccfded9578353f311967da7", - "reference": "fc4c04bfd17130a9dccfded9578353f311967da7", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/386a294d621576302e7cc36965d6ed53b8c73c4f", + "reference": "386a294d621576302e7cc36965d6ed53b8c73c4f", "shasum": "" }, "require": { @@ -4462,7 +4488,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -4489,27 +4515,33 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-06-02T09:51:43+00:00" }, { "name": "symfony/config", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "e5533fcc0b3dd377626153b2852707878f363728" + "reference": "a094618deb9a3fe1c3cf500a796e167d0495a274" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/e5533fcc0b3dd377626153b2852707878f363728", - "reference": "e5533fcc0b3dd377626153b2852707878f363728", + "url": "https://api.github.com/repos/symfony/config/zipball/a094618deb9a3fe1c3cf500a796e167d0495a274", + "reference": "a094618deb9a3fe1c3cf500a796e167d0495a274", "shasum": "" }, "require": { "php": ">=5.5.9", "symfony/filesystem": "~2.8|~3.0" }, + "conflict": { + "symfony/dependency-injection": "<3.3", + "symfony/finder": "<3.3" + }, "require-dev": { + "symfony/dependency-injection": "~3.3", + "symfony/finder": "~3.3", "symfony/yaml": "~3.0" }, "suggest": { @@ -4518,7 +4550,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -4545,7 +4577,7 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-06-16T12:40:34+00:00" }, { "name": "symfony/dom-crawler", @@ -4605,16 +4637,16 @@ }, { "name": "symfony/filesystem", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "040651db13cf061827a460cc10f6e36a445c45b4" + "reference": "311fa718389efbd8b627c272b9324a62437018cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/040651db13cf061827a460cc10f6e36a445c45b4", - "reference": "040651db13cf061827a460cc10f6e36a445c45b4", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/311fa718389efbd8b627c272b9324a62437018cc", + "reference": "311fa718389efbd8b627c272b9324a62437018cc", "shasum": "" }, "require": { @@ -4623,7 +4655,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -4650,20 +4682,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-06-24T09:29:48+00:00" }, { "name": "symfony/stopwatch", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a0105afb670dbd38f521105c444de1b8e10cfe3" + "reference": "602a15299dc01556013b07167d4f5d3a60e90d15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a0105afb670dbd38f521105c444de1b8e10cfe3", - "reference": "5a0105afb670dbd38f521105c444de1b8e10cfe3", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/602a15299dc01556013b07167d4f5d3a60e90d15", + "reference": "602a15299dc01556013b07167d4f5d3a60e90d15", "shasum": "" }, "require": { @@ -4672,7 +4704,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -4699,20 +4731,20 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-04-12T14:14:56+00:00" }, { "name": "symfony/yaml", - "version": "v3.2.8", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6" + "reference": "1f93a8d19b8241617f5074a123e282575b821df8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/acec26fcf7f3031e094e910b94b002fa53d4e4d6", - "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1f93a8d19b8241617f5074a123e282575b821df8", + "reference": "1f93a8d19b8241617f5074a123e282575b821df8", "shasum": "" }, "require": { @@ -4727,7 +4759,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -4754,7 +4786,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-05-01T14:55:58+00:00" + "time": "2017-06-15T12:58:50+00:00" }, { "name": "webmozart/assert", diff --git a/config/csv.php b/config/csv.php index 5da40e41e0..cc23de32f1 100644 --- a/config/csv.php +++ b/config/csv.php @@ -292,6 +292,7 @@ return [ // number of example rows: 'example_rows' => 5, 'default_config' => [ + 'initial-config-complete' => false, 'has-headers' => false, // assume 'date-format' => 'Ymd', // assume 'delimiter' => ',', // assume diff --git a/config/firefly.php b/config/firefly.php index cb76de0723..5fffbbf7a8 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -23,15 +23,21 @@ return [ 'is_demo_site' => false, ], 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), - 'version' => '4.4.3', - 'maxUploadSize' => 5242880, + 'version' => '4.6.2', + 'maxUploadSize' => 15242880, 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'], 'list_length' => 10, 'export_formats' => [ 'csv' => 'FireflyIII\Export\Exporter\CsvExporter', ], 'import_formats' => [ - 'csv' => 'FireflyIII\Import\Importer\CsvImporter', + 'csv' => 'FireflyIII\Import\Configurator\CsvConfigurator', + ], + 'import_configurators' => [ + 'csv' => 'FireflyIII\Import\Configurator\CsvConfigurator', + ], + 'import_processors' => [ + 'csv' => 'FireflyIII\Import\FileProcessor\CsvProcessor', ], 'default_export_format' => 'csv', 'default_import_format' => 'csv', diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 283a342b20..c014cb7078 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -89,6 +89,7 @@ $factory->define( 'user_id' => 1, 'transaction_type_id' => 1, 'bill_id' => null, + // TODO update this transaction currency reference. 'transaction_currency_id' => 1, 'description' => $faker->words(3, true), 'date' => '2017-01-01', @@ -216,25 +217,31 @@ $factory->define( $factory->define( FireflyIII\Models\Transaction::class, function (Faker\Generator $faker) { return [ - 'transaction_amount' => strval($faker->randomFloat(2, -100, 100)), - 'destination_amount' => strval($faker->randomFloat(2, -100, 100)), - 'opposing_account_id' => $faker->numberBetween(1, 10), - 'source_account_id' => $faker->numberBetween(1, 10), - 'opposing_account_name' => $faker->words(3, true), - 'description' => $faker->words(3, true), - 'source_account_name' => $faker->words(3, true), - 'destination_account_id' => $faker->numberBetween(1, 10), - 'date' => new Carbon, - 'destination_account_name' => $faker->words(3, true), - 'amount' => strval($faker->randomFloat(2, -100, 100)), - 'budget_id' => 0, - 'category' => $faker->words(3, true), - 'transaction_journal_id' => $faker->numberBetween(1, 10), - 'journal_id' => $faker->numberBetween(1, 10), - 'transaction_currency_code' => 'EUR', - 'transaction_type_type' => 'Withdrawal', - 'account_encrypted' => 0, - 'account_name' => 'Some name', + 'transaction_amount' => strval($faker->randomFloat(2, -100, 100)), + 'destination_amount' => strval($faker->randomFloat(2, -100, 100)), + 'opposing_account_id' => $faker->numberBetween(1, 10), + 'source_account_id' => $faker->numberBetween(1, 10), + 'opposing_account_name' => $faker->words(3, true), + 'description' => $faker->words(3, true), + 'source_account_name' => $faker->words(3, true), + 'destination_account_id' => $faker->numberBetween(1, 10), + 'date' => new Carbon, + 'destination_account_name' => $faker->words(3, true), + 'amount' => strval($faker->randomFloat(2, -100, 100)), + 'budget_id' => 0, + 'category' => $faker->words(3, true), + 'transaction_journal_id' => $faker->numberBetween(1, 10), + 'journal_id' => $faker->numberBetween(1, 10), + 'transaction_currency_code' => 'EUR', + 'transaction_type_type' => 'Withdrawal', + 'account_encrypted' => 0, + 'account_name' => 'Some name', + 'transaction_currency_id' => 1, + 'transaction_currency_symbol' => '€', + 'foreign_destination_amount' => null, + 'foreign_currency_id' => null, + 'foreign_currency_code' => null, + 'foreign_currency_symbol' => null, ]; } -); \ No newline at end of file +); diff --git a/database/migrations/2016_06_16_000002_create_main_tables.php b/database/migrations/2016_06_16_000002_create_main_tables.php index 0f5fe8d4e4..300024918a 100644 --- a/database/migrations/2016_06_16_000002_create_main_tables.php +++ b/database/migrations/2016_06_16_000002_create_main_tables.php @@ -474,7 +474,7 @@ class CreateMainTables extends Migration $table->text('description')->nullable(); $table->decimal('latitude', 24, 12)->nullable(); $table->decimal('longitude', 24, 12)->nullable(); - $table->boolean('zoomLevel')->nullable(); + $table->smallInteger('zoomLevel', false, true)->nullable(); // link user id to users table $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); diff --git a/database/migrations/2017_06_02_105232_changes_for_v450.php b/database/migrations/2017_06_02_105232_changes_for_v450.php new file mode 100644 index 0000000000..1f44bedf52 --- /dev/null +++ b/database/migrations/2017_06_02_105232_changes_for_v450.php @@ -0,0 +1,42 @@ +decimal('foreign_amount', 22, 12)->nullable()->after('amount'); + } + ); + + // add foreign transaction currency id to transactions (is nullable): + Schema::table( + 'transactions', function (Blueprint $table) { + $table->integer('foreign_currency_id', false, true)->default(null)->after('foreign_amount')->nullable(); + $table->foreign('foreign_currency_id')->references('id')->on('transaction_currencies')->onDelete('set null'); + } + ); + } +} diff --git a/database/seeds/TransactionCurrencySeeder.php b/database/seeds/TransactionCurrencySeeder.php index 8f51c69cd1..0d7732e80b 100644 --- a/database/seeds/TransactionCurrencySeeder.php +++ b/database/seeds/TransactionCurrencySeeder.php @@ -28,6 +28,7 @@ class TransactionCurrencySeeder extends Seeder TransactionCurrency::create(['code' => 'GBP', 'name' => 'British Pound', 'symbol' => '£', 'decimal_places' => 2]); TransactionCurrency::create(['code' => 'IDR', 'name' => 'Indonesian rupiah', 'symbol' => 'Rp', 'decimal_places' => 2]); TransactionCurrency::create(['code' => 'XBT', 'name' => 'Bitcoin', 'symbol' => 'B', 'decimal_places' => 8]); + TransactionCurrency::create(['code' => 'JPY', 'name' => 'Japanese yen', 'symbol' => '¥', 'decimal_places' => 2]); } } diff --git a/public/browserconfig.xml b/public/browserconfig.xml index 74bb89ac30..b3930d0f04 100644 --- a/public/browserconfig.xml +++ b/public/browserconfig.xml @@ -1,9 +1,9 @@ - - - - #da532c - - + + + + #da532c + + diff --git a/public/css/bootstrap-sortable.css b/public/css/bootstrap-sortable.css index aed89cd62e..fe3fe8ab05 100755 --- a/public/css/bootstrap-sortable.css +++ b/public/css/bootstrap-sortable.css @@ -43,20 +43,20 @@ table.sortable span.arrow, span.reversed, th.arrow.down:after, th.reversedarrow. margin-top: -2px; } - table.sortable span.arrow.up, th.arrow.up:after { - border-color: transparent transparent #ccc transparent; - margin-top: -7px; - } +table.sortable span.arrow.up, th.arrow.up:after { + border-color: transparent transparent #ccc transparent; + margin-top: -7px; +} table.sortable span.reversed, th.reversedarrow.down:after { border-color: transparent transparent #ccc transparent; margin-top: -7px; } - table.sortable span.reversed.up, th.reversedarrow.up:after { - border-color: #ccc transparent transparent transparent; - margin-top: -2px; - } +table.sortable span.reversed.up, th.reversedarrow.up:after { + border-color: #ccc transparent transparent transparent; + margin-top: -2px; +} table.sortable span.az:before, th.az.down:after { content: "a .. z"; @@ -94,17 +94,17 @@ table.sortable span.month.up:before, th.month.up:after { content: "dec .. jan"; } -table.sortable>thead th:not([data-defaultsort=disabled]) { +table.sortable > thead th:not([data-defaultsort=disabled]) { cursor: pointer; position: relative; top: 0; left: 0; } -table.sortable>thead th:hover:not([data-defaultsort=disabled]) { +table.sortable > thead th:hover:not([data-defaultsort=disabled]) { background: #efefef; } -table.sortable>thead th div.mozilla { +table.sortable > thead th div.mozilla { position: relative; } diff --git a/public/css/bootstrap-tagsinput.css b/public/css/bootstrap-tagsinput.css index 7fced3009d..e8f37d2d64 100755 --- a/public/css/bootstrap-tagsinput.css +++ b/public/css/bootstrap-tagsinput.css @@ -4,57 +4,67 @@ */ .bootstrap-tagsinput { - background-color: #fff; - border: 1px solid #ccc; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - display: inline-block; - padding: 4px 6px; - color: #555; - vertical-align: middle; - border-radius: 4px; - max-width: 100%; - line-height: 22px; - cursor: text; + background-color: #fff; + border: 1px solid #ccc; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + display: inline-block; + padding: 4px 6px; + color: #555; + vertical-align: middle; + border-radius: 4px; + max-width: 100%; + line-height: 22px; + cursor: text; } + .bootstrap-tagsinput input { - border: none; - box-shadow: none; - outline: none; - background-color: transparent; - padding: 0 6px; - margin: 0; - width: auto; - max-width: inherit; + border: none; + box-shadow: none; + outline: none; + background-color: transparent; + padding: 0 6px; + margin: 0; + width: auto; + max-width: inherit; } + .bootstrap-tagsinput.form-control input::-moz-placeholder { - color: #777; - opacity: 1; + color: #777; + opacity: 1; } + .bootstrap-tagsinput.form-control input:-ms-input-placeholder { - color: #777; + color: #777; } + .bootstrap-tagsinput.form-control input::-webkit-input-placeholder { - color: #777; + color: #777; } + .bootstrap-tagsinput input:focus { - border: none; - box-shadow: none; + border: none; + box-shadow: none; } + .bootstrap-tagsinput .tag { - margin-right: 2px; - color: white; + margin-right: 2px; + color: white; } + .bootstrap-tagsinput .tag [data-role="remove"] { - margin-left: 8px; - cursor: pointer; + margin-left: 8px; + cursor: pointer; } + .bootstrap-tagsinput .tag [data-role="remove"]:after { - content: "x"; - padding: 0px 2px; + content: "x"; + padding: 0px 2px; } + .bootstrap-tagsinput .tag [data-role="remove"]:hover { - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); } + .bootstrap-tagsinput .tag [data-role="remove"]:hover:active { - box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); } diff --git a/public/css/bootstrap-tour.min.css b/public/css/bootstrap-tour.min.css deleted file mode 100755 index 8ebd3d3750..0000000000 --- a/public/css/bootstrap-tour.min.css +++ /dev/null @@ -1,22 +0,0 @@ -/* ======================================================================== - * bootstrap-tour - v0.10.3 - * http://bootstraptour.com - * ======================================================================== - * Copyright 2012-2015 Ulrich Sossou - * - * ======================================================================== - * Licensed under the MIT License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/MIT - * - * 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. - * ======================================================================== - */ - -.tour-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1100;background-color:#000;opacity:.8;filter:alpha(opacity=80)}.tour-step-backdrop{position:relative;z-index:1101}.tour-step-backdrop>td{position:relative;z-index:1101}.tour-step-background{position:absolute!important;z-index:1100;background:inherit;border-radius:6px}.popover[class*=tour-]{z-index:1102}.popover[class*=tour-] .popover-navigation{padding:9px 14px;overflow:hidden}.popover[class*=tour-] .popover-navigation [data-role=end]{float:right}.popover[class*=tour-] .popover-navigation [data-role=prev],.popover[class*=tour-] .popover-navigation [data-role=next],.popover[class*=tour-] .popover-navigation [data-role=end]{cursor:pointer}.popover[class*=tour-] .popover-navigation [data-role=prev].disabled,.popover[class*=tour-] .popover-navigation [data-role=next].disabled,.popover[class*=tour-] .popover-navigation [data-role=end].disabled{cursor:default}.popover[class*=tour-].orphan{position:fixed;margin-top:0}.popover[class*=tour-].orphan .arrow{display:none} \ No newline at end of file diff --git a/public/css/daterangepicker.css b/public/css/daterangepicker.css index a03ff440aa..23fb4e1c69 100755 --- a/public/css/daterangepicker.css +++ b/public/css/daterangepicker.css @@ -1,141 +1,213 @@ .daterangepicker { - position: absolute; - color: inherit; - background: #fff; - border-radius: 4px; - width: 278px; - padding: 4px; - margin-top: 1px; - top: 100px; - left: 20px; - /* Calendars */ } - .daterangepicker:before, .daterangepicker:after { + position: absolute; + color: inherit; + background: #fff; + border-radius: 4px; + width: 278px; + padding: 4px; + margin-top: 1px; + top: 100px; + left: 20px; + /* Calendars */ +} + +.daterangepicker:before, .daterangepicker:after { position: absolute; display: inline-block; border-bottom-color: rgba(0, 0, 0, 0.2); - content: ''; } - .daterangepicker:before { + content: ''; +} + +.daterangepicker:before { top: -7px; border-right: 7px solid transparent; border-left: 7px solid transparent; - border-bottom: 7px solid #ccc; } - .daterangepicker:after { + border-bottom: 7px solid #ccc; +} + +.daterangepicker:after { top: -6px; border-right: 6px solid transparent; border-bottom: 6px solid #fff; - border-left: 6px solid transparent; } - .daterangepicker.opensleft:before { - right: 9px; } - .daterangepicker.opensleft:after { - right: 10px; } - .daterangepicker.openscenter:before { + border-left: 6px solid transparent; +} + +.daterangepicker.opensleft:before { + right: 9px; +} + +.daterangepicker.opensleft:after { + right: 10px; +} + +.daterangepicker.openscenter:before { left: 0; right: 0; width: 0; margin-left: auto; - margin-right: auto; } - .daterangepicker.openscenter:after { + margin-right: auto; +} + +.daterangepicker.openscenter:after { left: 0; right: 0; width: 0; margin-left: auto; - margin-right: auto; } - .daterangepicker.opensright:before { - left: 9px; } - .daterangepicker.opensright:after { - left: 10px; } - .daterangepicker.dropup { - margin-top: -5px; } - .daterangepicker.dropup:before { - top: initial; - bottom: -7px; - border-bottom: initial; - border-top: 7px solid #ccc; } - .daterangepicker.dropup:after { - top: initial; - bottom: -6px; - border-bottom: initial; - border-top: 6px solid #fff; } - .daterangepicker.dropdown-menu { + margin-right: auto; +} + +.daterangepicker.opensright:before { + left: 9px; +} + +.daterangepicker.opensright:after { + left: 10px; +} + +.daterangepicker.dropup { + margin-top: -5px; +} + +.daterangepicker.dropup:before { + top: initial; + bottom: -7px; + border-bottom: initial; + border-top: 7px solid #ccc; +} + +.daterangepicker.dropup:after { + top: initial; + bottom: -6px; + border-bottom: initial; + border-top: 6px solid #fff; +} + +.daterangepicker.dropdown-menu { max-width: none; - z-index: 3001; } - .daterangepicker.single .ranges, .daterangepicker.single .calendar { - float: none; } - .daterangepicker.show-calendar .calendar { - display: block; } - .daterangepicker .calendar { + z-index: 3001; +} + +.daterangepicker.single .ranges, .daterangepicker.single .calendar { + float: none; +} + +.daterangepicker.show-calendar .calendar { + display: block; +} + +.daterangepicker .calendar { display: none; max-width: 270px; - margin: 4px; } - .daterangepicker .calendar.single .calendar-table { - border: none; } - .daterangepicker .calendar th, .daterangepicker .calendar td { - white-space: nowrap; - text-align: center; - min-width: 32px; } - .daterangepicker .calendar-table { + margin: 4px; +} + +.daterangepicker .calendar.single .calendar-table { + border: none; +} + +.daterangepicker .calendar th, .daterangepicker .calendar td { + white-space: nowrap; + text-align: center; + min-width: 32px; +} + +.daterangepicker .calendar-table { border: 1px solid #fff; padding: 4px; border-radius: 4px; - background: #fff; } - .daterangepicker table { + background: #fff; +} + +.daterangepicker table { width: 100%; - margin: 0; } - .daterangepicker td, .daterangepicker th { + margin: 0; +} + +.daterangepicker td, .daterangepicker th { text-align: center; width: 20px; height: 20px; border-radius: 4px; border: 1px solid transparent; white-space: nowrap; - cursor: pointer; } - .daterangepicker td.available:hover, .daterangepicker th.available:hover { - background-color: #eee; - border-color: transparent; - color: inherit; } - .daterangepicker td.week, .daterangepicker th.week { - font-size: 80%; - color: #ccc; } - .daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { + cursor: pointer; +} + +.daterangepicker td.available:hover, .daterangepicker th.available:hover { + background-color: #eee; + border-color: transparent; + color: inherit; +} + +.daterangepicker td.week, .daterangepicker th.week { + font-size: 80%; + color: #ccc; +} + +.daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { background-color: #fff; border-color: transparent; - color: #999; } - .daterangepicker td.in-range { + color: #999; +} + +.daterangepicker td.in-range { background-color: #ebf4f8; border-color: transparent; color: #000; - border-radius: 0; } - .daterangepicker td.start-date { - border-radius: 4px 0 0 4px; } - .daterangepicker td.end-date { - border-radius: 0 4px 4px 0; } - .daterangepicker td.start-date.end-date { - border-radius: 4px; } - .daterangepicker td.active, .daterangepicker td.active:hover { + border-radius: 0; +} + +.daterangepicker td.start-date { + border-radius: 4px 0 0 4px; +} + +.daterangepicker td.end-date { + border-radius: 0 4px 4px 0; +} + +.daterangepicker td.start-date.end-date { + border-radius: 4px; +} + +.daterangepicker td.active, .daterangepicker td.active:hover { background-color: #357ebd; border-color: transparent; - color: #fff; } - .daterangepicker th.month { - width: auto; } - .daterangepicker td.disabled, .daterangepicker option.disabled { + color: #fff; +} + +.daterangepicker th.month { + width: auto; +} + +.daterangepicker td.disabled, .daterangepicker option.disabled { color: #999; cursor: not-allowed; - text-decoration: line-through; } - .daterangepicker select.monthselect, .daterangepicker select.yearselect { + text-decoration: line-through; +} + +.daterangepicker select.monthselect, .daterangepicker select.yearselect { font-size: 12px; padding: 1px; height: auto; margin: 0; - cursor: default; } - .daterangepicker select.monthselect { + cursor: default; +} + +.daterangepicker select.monthselect { margin-right: 2%; - width: 56%; } - .daterangepicker select.yearselect { - width: 40%; } - .daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { + width: 56%; +} + +.daterangepicker select.yearselect { + width: 40%; +} + +.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { width: 50px; - margin-bottom: 0; } - .daterangepicker .input-mini { + margin-bottom: 0; +} + +.daterangepicker .input-mini { border: 1px solid #ccc; border-radius: 4px; color: #555; @@ -145,43 +217,62 @@ vertical-align: middle; margin: 0 0 5px 0; padding: 0 6px 0 28px; - width: 100%; } - .daterangepicker .input-mini.active { - border: 1px solid #08c; - border-radius: 4px; } - .daterangepicker .daterangepicker_input { - position: relative; } - .daterangepicker .daterangepicker_input i { - position: absolute; - left: 8px; - top: 8px; } - .daterangepicker.rtl .input-mini { + width: 100%; +} + +.daterangepicker .input-mini.active { + border: 1px solid #08c; + border-radius: 4px; +} + +.daterangepicker .daterangepicker_input { + position: relative; +} + +.daterangepicker .daterangepicker_input i { + position: absolute; + left: 8px; + top: 8px; +} + +.daterangepicker.rtl .input-mini { padding-right: 28px; - padding-left: 6px; } - .daterangepicker.rtl .daterangepicker_input i { + padding-left: 6px; +} + +.daterangepicker.rtl .daterangepicker_input i { left: auto; - right: 8px; } - .daterangepicker .calendar-time { + right: 8px; +} + +.daterangepicker .calendar-time { text-align: center; margin: 5px auto; line-height: 30px; position: relative; - padding-left: 28px; } - .daterangepicker .calendar-time select.disabled { - color: #ccc; - cursor: not-allowed; } + padding-left: 28px; +} + +.daterangepicker .calendar-time select.disabled { + color: #ccc; + cursor: not-allowed; +} .ranges { - font-size: 11px; - float: none; - margin: 4px; - text-align: left; } - .ranges ul { + font-size: 11px; + float: none; + margin: 4px; + text-align: left; +} + +.ranges ul { list-style: none; margin: 0 auto; padding: 0; - width: 100%; } - .ranges li { + width: 100%; +} + +.ranges li { font-size: 13px; background: #f5f5f5; border: 1px solid #f5f5f5; @@ -189,81 +280,139 @@ color: #08c; padding: 3px 12px; margin-bottom: 8px; - cursor: pointer; } - .ranges li:hover { - background: #08c; - border: 1px solid #08c; - color: #fff; } - .ranges li.active { - background: #08c; - border: 1px solid #08c; - color: #fff; } + cursor: pointer; +} + +.ranges li:hover { + background: #08c; + border: 1px solid #08c; + color: #fff; +} + +.ranges li.active { + background: #08c; + border: 1px solid #08c; + color: #fff; +} /* Larger Screen Styling */ @media (min-width: 564px) { - .daterangepicker { - width: auto; } + .daterangepicker { + width: auto; + } + .daterangepicker .ranges ul { - width: 160px; } + width: 160px; + } + .daterangepicker.single .ranges ul { - width: 100%; } + width: 100%; + } + .daterangepicker.single .calendar.left { - clear: none; } + clear: none; + } + .daterangepicker.single.ltr .ranges, .daterangepicker.single.ltr .calendar { - float: left; } + float: left; + } + .daterangepicker.single.rtl .ranges, .daterangepicker.single.rtl .calendar { - float: right; } + float: right; + } + .daterangepicker.ltr { - direction: ltr; - text-align: left; } - .daterangepicker.ltr .calendar.left { + direction: ltr; + text-align: left; + } + + .daterangepicker.ltr .calendar.left { clear: left; - margin-right: 0; } - .daterangepicker.ltr .calendar.left .calendar-table { - border-right: none; - border-top-right-radius: 0; - border-bottom-right-radius: 0; } - .daterangepicker.ltr .calendar.right { - margin-left: 0; } - .daterangepicker.ltr .calendar.right .calendar-table { - border-left: none; - border-top-left-radius: 0; - border-bottom-left-radius: 0; } - .daterangepicker.ltr .left .daterangepicker_input { - padding-right: 12px; } - .daterangepicker.ltr .calendar.left .calendar-table { - padding-right: 12px; } - .daterangepicker.ltr .ranges, .daterangepicker.ltr .calendar { - float: left; } + margin-right: 0; + } + + .daterangepicker.ltr .calendar.left .calendar-table { + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + .daterangepicker.ltr .calendar.right { + margin-left: 0; + } + + .daterangepicker.ltr .calendar.right .calendar-table { + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + .daterangepicker.ltr .left .daterangepicker_input { + padding-right: 12px; + } + + .daterangepicker.ltr .calendar.left .calendar-table { + padding-right: 12px; + } + + .daterangepicker.ltr .ranges, .daterangepicker.ltr .calendar { + float: left; + } + .daterangepicker.rtl { - direction: rtl; - text-align: right; } - .daterangepicker.rtl .calendar.left { - clear: right; - margin-left: 0; } - .daterangepicker.rtl .calendar.left .calendar-table { - border-left: none; - border-top-left-radius: 0; - border-bottom-left-radius: 0; } - .daterangepicker.rtl .calendar.right { - margin-right: 0; } - .daterangepicker.rtl .calendar.right .calendar-table { - border-right: none; - border-top-right-radius: 0; - border-bottom-right-radius: 0; } - .daterangepicker.rtl .left .daterangepicker_input { - padding-left: 12px; } - .daterangepicker.rtl .calendar.left .calendar-table { - padding-left: 12px; } - .daterangepicker.rtl .ranges, .daterangepicker.rtl .calendar { + direction: rtl; text-align: right; - float: right; } } + } + + .daterangepicker.rtl .calendar.left { + clear: right; + margin-left: 0; + } + + .daterangepicker.rtl .calendar.left .calendar-table { + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + .daterangepicker.rtl .calendar.right { + margin-right: 0; + } + + .daterangepicker.rtl .calendar.right .calendar-table { + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + .daterangepicker.rtl .left .daterangepicker_input { + padding-left: 12px; + } + + .daterangepicker.rtl .calendar.left .calendar-table { + padding-left: 12px; + } + + .daterangepicker.rtl .ranges, .daterangepicker.rtl .calendar { + text-align: right; + float: right; + } +} + @media (min-width: 730px) { - .daterangepicker .ranges { - width: auto; } - .daterangepicker.ltr .ranges { - float: left; } - .daterangepicker.rtl .ranges { - float: right; } - .daterangepicker .calendar.left { - clear: none !important; } } + .daterangepicker .ranges { + width: auto; + } + + .daterangepicker.ltr .ranges { + float: left; + } + + .daterangepicker.rtl .ranges { + float: right; + } + + .daterangepicker .calendar.left { + clear: none !important; + } +} diff --git a/public/index.php b/public/index.php index 7f21d36fb5..dbab349937 100644 --- a/public/index.php +++ b/public/index.php @@ -9,7 +9,7 @@ * See the LICENSE file for details. */ -declare(strict_types = 1); +declare(strict_types=1); /* |-------------------------------------------------------------------------- @@ -23,7 +23,7 @@ declare(strict_types = 1); | */ -require __DIR__.'/../bootstrap/autoload.php'; +require __DIR__ . '/../bootstrap/autoload.php'; /* |-------------------------------------------------------------------------- @@ -37,7 +37,7 @@ require __DIR__.'/../bootstrap/autoload.php'; | */ -$app = require_once __DIR__.'/../bootstrap/app.php'; +$app = require_once __DIR__ . '/../bootstrap/app.php'; /* |-------------------------------------------------------------------------- diff --git a/public/js/ff/budgets/index.js b/public/js/ff/budgets/index.js index 936ae07b7f..79dd675ecd 100644 --- a/public/js/ff/budgets/index.js +++ b/public/js/ff/budgets/index.js @@ -8,7 +8,7 @@ * See the LICENSE file for details. */ -/** global: spent, budgeted, available, currencySymbol */ +/** global: spent, budgeted, available, currencySymbol, budgetIndexURI, accounting */ function drawSpentBar() { "use strict"; @@ -59,9 +59,26 @@ function updateBudgetedAmounts(e) { "use strict"; var target = $(e.target); var id = target.data('id'); + var value = target.val(); var original = target.data('original'); var difference = value - original; + + var spentCell = $('td[class="spent"][data-id="' + id + '"]'); + var leftCell = $('td[class="left"][data-id="' + id + '"]'); + var spentAmount = parseFloat(spentCell.data('spent')); + var newAmountLeft = spentAmount + parseFloat(value); + var amountLeftString = accounting.formatMoney(newAmountLeft); + if (newAmountLeft < 0) { + leftCell.html('' + amountLeftString + ''); + } + if (newAmountLeft > 0) { + leftCell.html('' + amountLeftString + ''); + } + if (newAmountLeft === 0.0) { + leftCell.html('' + amountLeftString + ''); + } + if (difference !== 0) { // add difference to 'budgeted' var budgeted = budgeted + difference; @@ -99,6 +116,15 @@ $(function () { */ $('input[type="number"]').on('input', updateBudgetedAmounts); + // + $('.selectPeriod').change(function (e) { + var sel = $(e.target).val(); + if (sel !== "x") { + var newURI = budgetIndexURI.replace("REPLACE", sel); + window.location.assign(newURI); + } + }); + }); function updateIncome() { diff --git a/public/js/ff/budgets/show.js b/public/js/ff/budgets/show.js index 9d276150df..78feb2e782 100644 --- a/public/js/ff/budgets/show.js +++ b/public/js/ff/budgets/show.js @@ -8,7 +8,7 @@ * See the LICENSE file for details. */ -/** global: budgetChartUri,budgetLimitID */ +/** global: budgetChartUri, expenseCategoryUri, expenseAssetUri, expenseExpenseUri, budgetLimitID */ $(function () { "use strict"; @@ -25,6 +25,4 @@ $(function () { pieChart(expenseExpenseUri, 'budget-expense-out'); - - }); diff --git a/public/js/ff/categories/show.js b/public/js/ff/categories/show.js index 9efadd8828..ed564154d9 100644 --- a/public/js/ff/categories/show.js +++ b/public/js/ff/categories/show.js @@ -13,10 +13,7 @@ $(function () { "use strict"; - console.log('Getting charts'); columnChart(everything, 'category-everything'); - - console.log('Specific: ' + specific); columnChart(specific, 'specific-period'); }); \ No newline at end of file diff --git a/public/js/ff/charts.js b/public/js/ff/charts.js index 35b99f0b00..5e5bf17ce0 100644 --- a/public/js/ff/charts.js +++ b/public/js/ff/charts.js @@ -75,7 +75,7 @@ function lineChart(URI, container) { "use strict"; var colorData = true; - var options = defaultChartOptions; + var options = $.extend(true, {}, defaultChartOptions); var chartType = 'line'; drawAChart(URI, container, chartType, options, colorData); @@ -186,9 +186,8 @@ function doubleYNonStackedChart(URI, container) { */ function columnChart(URI, container) { "use strict"; - console.log('Going to draw column chart for ' + URI + ' in ' + container); var colorData = true; - var options = defaultChartOptions; + var options = $.extend(true, {}, defaultChartOptions); var chartType = 'bar'; drawAChart(URI, container, chartType, options, colorData); @@ -224,7 +223,7 @@ function pieChart(URI, container) { "use strict"; var colorData = false; - var options = defaultPieOptions; + var options = $.extend(true, {}, defaultPieOptions); var chartType = 'pie'; drawAChart(URI, container, chartType, options, colorData); diff --git a/public/js/ff/export/index.js b/public/js/ff/export/index.js index c7d0f3fd40..c04d89f2cc 100644 --- a/public/js/ff/export/index.js +++ b/public/js/ff/export/index.js @@ -104,10 +104,22 @@ function callExport() { // show download showDownload(); - }).fail(function () { + }).fail(function (data) { // show error. // show form again. - showError('The export failed. Please check the log files to find out why.'); + + var errorText = 'The export failed. Please check the log files to find out why.'; + if (typeof data.responseJSON === 'object') { + errorText = ''; + for (var propt in data.responseJSON) { + if (data.responseJSON.hasOwnProperty(propt)) { + errorText += propt + ': ' + data.responseJSON[propt][0]; + } + } + } + + showError(errorText); + // stop polling: window.clearTimeout(intervalId); diff --git a/public/js/ff/firefly.js b/public/js/ff/firefly.js index c0a223edf0..3015c3d49e 100644 --- a/public/js/ff/firefly.js +++ b/public/js/ff/firefly.js @@ -15,6 +15,11 @@ $(function () { configAccounting(currencySymbol); + // on submit of form, disable any button in form: + $('form.form-horizontal').on('submit',function(e) { + $('button[type="submit"]').prop('disabled',true); + }); + $.ajaxSetup({ headers: { 'X-CSRF-Token': $('meta[name="_token"]').attr('content') diff --git a/public/js/ff/import/status.js b/public/js/ff/import/status.js index cc1ec69a9e..f8a991074d 100644 --- a/public/js/ff/import/status.js +++ b/public/js/ff/import/status.js @@ -10,49 +10,164 @@ /** global: jobImportUrl, langImportSingleError, langImportMultiError, jobStartUrl, langImportTimeOutError, langImportFinished, langImportFatalError */ -var startedImport = false; -var startInterval = 2000; +var timeOutId; +var startInterval = 1000; var interval = 500; -var timeoutLimit = 5000; -var currentLimit = 0; -var stepCount = 0; + +// these vars are used to detect a stalled job: +var numberOfSteps = 0; +var numberOfReports = 0; +var jobFailed = false; + +// counts how many errors have been detected +var knownErrors = 0; + $(function () { "use strict"; - - $('#import-status-intro').hide(); - $('#import-status-more-info').hide(); - - // check status, every 500 ms. - setTimeout(checkImportStatus, startInterval); - + timeOutId = setTimeout(checkImportStatus, startInterval); + $('.start-job').click(startJob); }); - +/** + * Downloads some JSON and responds to its content to see what the status is of the current import. + */ function checkImportStatus() { - "use strict"; $.getJSON(jobImportUrl).done(reportOnJobImport).fail(failedJobImport); } -function importComplete() { - "use strict"; - var bar = $('#import-status-bar'); - bar.removeClass('active'); +/** + * This method is called when the JSON query returns an error. If possible, this error is relayed to the user. + */ +function failedJobImport(jqxhr, textStatus, error) { + // hide all possible boxes: + $('.statusbox').hide(); + + // fill in some details: + var errorMessage = textStatus + " " + error; + + $('.fatal_error_txt').text(errorMessage); + + // show the fatal error box: + $('.fatal_error').show(); } +/** + * This method is called when the job enquiry (JSON) returns some info. + * It also decides whether or not to check again. + * + * @param data + */ +function reportOnJobImport(data) { + + switch (data.status) { + case "configured": + // job is ready. Do not check again, just show the start-box. Hide the rest. + $('.statusbox').hide(); + $('.status_configured').show(); + break; + case "running": + // job is running! Show the running box: + $('.statusbox').hide(); + $('.status_running').show(); + + // update the bar + updateBar(data); + + // update the status text: + updateStatusText(data); + + // report on detected errors: + reportOnErrors(data); + + if (jobIsStalled(data)) { + // do something + showStalledBox(); + } else { + // check again in 500ms + timeOutId = setTimeout(checkImportStatus, interval); + } + break; + case "finished": + $('.statusbox').hide(); + $('.status_finished').show(); + // show text: + $('#import-status-more-info').html(data.finishedText); + + + break; + } +} + +/** + * Shows a fatal error when the job seems to be stalled. + */ +function showStalledBox() { + $('.statusbox').hide(); + $('.fatal_error').show(); + $('.fatal_error_txt').text(langImportTimeOutError); +} + +/** + * Detects if a job is frozen. + * + * @param data + */ +function jobIsStalled(data) { + if (data.done === numberOfSteps) { + numberOfReports++; + } + if (data.done !== numberOfSteps) { + numberOfReports = 0; + } + if (numberOfReports > 20) { + return true; + } + numberOfSteps = data.done; + + return false; +} + +/** + * This function tells Firefly start the job. It will also initialize a re-check in 500ms time. + */ +function startJob() { + // disable the button, add loading thing. + $('.start-job').prop('disabled', true).text('...'); + $.post(jobStartUrl).fail(reportOnSubmitError); + + // check status, every 500 ms. + timeOutId = setTimeout(checkImportStatus, startInterval); +} + +function reportOnSubmitError() { + // stop the refresh thing + clearTimeout(timeOutId); + + // hide all possible boxes: + $('.statusbox').hide(); + + // fill in some details: + var errorMessage = "Time out while waiting for job to finish."; + + $('.fatal_error_txt').text(errorMessage); + + // show the fatal error box: + $('.fatal_error').show(); + jobFailed = true; + +} + +/** + * This method updates the percentage bar thing if the job is running! + */ function updateBar(data) { - "use strict"; var bar = $('#import-status-bar'); - if (data.showPercentage) { + if (data.show_percentage) { bar.addClass('progress-bar-success').removeClass('progress-bar-info'); bar.attr('aria-valuenow', data.percentage); bar.css('width', data.percentage + '%'); - $('#import-status-bar').text(data.stepsDone + '/' + data.steps); - - if (data.percentage >= 100) { - importComplete(); - return; - } - return; + $('#import-status-bar').text(data.done + '/' + data.steps); + return true; } // dont show percentage: bar.removeClass('progress-bar-success').addClass('progress-bar-info'); @@ -60,8 +175,27 @@ function updateBar(data) { bar.css('width', '100%'); } -function reportErrors(data) { +/** + * Add text with current import status. + * @param data + */ +function updateStatusText(data) { "use strict"; + $('#import-status-txt').removeClass('text-danger').text(data.statusText); +} + +/** + * Report on errors found in import: + * @param data + */ +function reportOnErrors(data) { + if (knownErrors === data.errors.length) { + return; + } + if (data.errors.length === 0) { + return; + } + if (data.errors.length === 1) { $('#import-status-error-intro').text(langImportSingleError); //'An error has occured during the import. The import can continue, however.' @@ -70,108 +204,16 @@ function reportErrors(data) { // 'Errors have occured during the import. The import can continue, however.' $('#import-status-error-intro').text(langImportMultiError); } - + $('.info_errors').show(); // fill the list with error texts $('#import-status-error-list').empty(); for (var i = 0; i < data.errors.length; i++) { - var item = $('
  • ').html(data.errors[i]); - $('#import-status-error-list').append(item); + var errorSet = data.errors[i]; + for (var j = 0; j < errorSet.length; j++) { + var item = $('
  • ').html(errorSet[j]); + $('#import-status-error-list').append(item); + } } -} + return; -function reportStatus(data) { - "use strict"; - $('#import-status-txt').removeClass('text-danger').text(data.statusText); -} - -function kickStartJob() { - "use strict"; - $.post(jobStartUrl); - startedTheImport(); - startedImport = true; -} - -function updateTimeout(data) { - "use strict"; - if (data.stepsDone !== stepCount) { - stepCount = data.stepsDone; - currentLimit = 0; - return; - } - - currentLimit = currentLimit + interval; -} - -function timeoutError() { - "use strict"; - // set status - $('#import-status-txt').addClass('text-danger').text(langImportTimeOutError); - - // remove progress bar. - $('#import-status-holder').hide(); - -} - -function importJobFinished(data) { - "use strict"; - return data.finished; -} - -function finishedJob(data) { - "use strict"; - // "There was an error during the import routine. Please check the log files. The error seems to be: '" - $('#import-status-txt').removeClass('text-danger').addClass('text-success').text(langImportFinished); - - // remove progress bar. - $('#import-status-holder').hide(); - - // show info: - $('#import-status-intro').show(); - $('#import-status-more-info').html(data.finishedText).show(); - -} - -function reportOnJobImport(data) { - "use strict"; - updateBar(data); - reportErrors(data); - reportStatus(data); - updateTimeout(data); - - if (importJobFinished(data)) { - finishedJob(data); - return; - } - - - // same number of steps as last time? - if (currentLimit > timeoutLimit) { - timeoutError(); - return; - } - - // if the job has not actually started, do so now: - if (!data.started && !startedImport) { - kickStartJob(); - return; - } - - // trigger another check. - setTimeout(checkImportStatus, interval); - -} - -function startedTheImport() { - "use strict"; - setTimeout(checkImportStatus, interval); -} - -function failedJobImport(jqxhr, textStatus, error) { - "use strict"; - // set status - // "There was an error during the import routine. Please check the log files. The error seems to be: '" - $('#import-status-txt').addClass('text-danger').text(langImportFatalError + ' ' + textStatus + ' ' + error); - - // remove progress bar. - $('#import-status-holder').hide(); } \ No newline at end of file diff --git a/public/js/ff/index.js b/public/js/ff/index.js index 242768712d..58005bac67 100644 --- a/public/js/ff/index.js +++ b/public/js/ff/index.js @@ -8,36 +8,15 @@ * See the LICENSE file for details. */ -/** global: Tour, showTour, accountFrontpageUri, token, billCount, accountExpenseUri, accountRevenueUri */ +/** global: accountFrontpageUri, token, billCount, accountExpenseUri, accountRevenueUri */ $(function () { "use strict"; // do chart JS stuff. drawChart(); - if (showTour === true) { - $.getJSON('json/tour').done(function (data) { - var tour = new Tour( - { - steps: data.steps, - template: data.template, - onEnd: endTheTour - }); - // Initialize the tour - tour.init(); - // Start the tour - tour.start(); - }); - } - }); -function endTheTour() { - "use strict"; - $.post('json/end-tour', {_token: token}); - -} - function drawChart() { "use strict"; lineChart(accountFrontpageUri, 'accounts-chart'); diff --git a/public/js/ff/intro/intro.js b/public/js/ff/intro/intro.js new file mode 100644 index 0000000000..42a2ef7cb6 --- /dev/null +++ b/public/js/ff/intro/intro.js @@ -0,0 +1,14 @@ +/* + * intro.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: route_for_tour */ + +$(function () { + "use strict"; + alert('show user intro for ' + route_for_tour); +}); \ No newline at end of file diff --git a/public/js/ff/reports/category/month.js b/public/js/ff/reports/category/month.js index 84f3444d9b..b07941a944 100644 --- a/public/js/ff/reports/category/month.js +++ b/public/js/ff/reports/category/month.js @@ -15,19 +15,19 @@ $(function () { drawChart(); $('#categories-in-pie-chart-checked').on('change', function () { - redrawPieChart('categories-in-pie-chart', categoryIncomeUri); + redrawPieChart(categoryIncomeUri, 'categories-in-pie-chart'); }); $('#categories-out-pie-chart-checked').on('change', function () { - redrawPieChart('categories-out-pie-chart', categoryExpenseUri); + redrawPieChart(categoryExpenseUri, 'categories-out-pie-chart'); }); $('#accounts-in-pie-chart-checked').on('change', function () { - redrawPieChart('accounts-in-pie-chart', accountIncomeUri); + redrawPieChart(accountIncomeUri, 'accounts-in-pie-chart'); }); $('#accounts-out-pie-chart-checked').on('change', function () { - redrawPieChart('accounts-out-pie-chart', accountExpenseUri); + redrawPieChart(accountExpenseUri, 'accounts-out-pie-chart'); }); }); @@ -40,15 +40,14 @@ function drawChart() { doubleYChart(mainUri, 'in-out-chart'); // draw pie chart of income, depending on "show other transactions too": - redrawPieChart('categories-in-pie-chart', categoryIncomeUri); - redrawPieChart('categories-out-pie-chart', categoryExpenseUri); - redrawPieChart('accounts-in-pie-chart', accountIncomeUri); - redrawPieChart('accounts-out-pie-chart', accountExpenseUri); - + redrawPieChart(categoryIncomeUri, 'categories-in-pie-chart'); + redrawPieChart(categoryExpenseUri, 'categories-out-pie-chart'); + redrawPieChart(accountIncomeUri, 'accounts-in-pie-chart'); + redrawPieChart(accountExpenseUri, 'accounts-out-pie-chart'); } -function redrawPieChart(container, uri) { +function redrawPieChart(uri, container) { "use strict"; var checkbox = $('#' + container + '-checked'); diff --git a/public/js/ff/search/index.js b/public/js/ff/search/index.js new file mode 100644 index 0000000000..06a8f26697 --- /dev/null +++ b/public/js/ff/search/index.js @@ -0,0 +1,35 @@ +/* + * index.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: searchQuery,searchUri */ + + + +$(function () { + "use strict"; + startSearch(searchQuery); + +}); + +function startSearch(query) { + + $.post(searchUri, {query: query}).done(presentSearchResults).fail(searchFailure); +} + +function searchFailure() { + $('.result_row').hide(); + $('.error_row').show(); +} + +function presentSearchResults(data) { + $('.search_ongoing').hide(); + $('p.search_count').show(); + $('span.search_count').text(data.count); + $('.search_box').find('.overlay').remove(); + $('.search_results').html(data.html).show(); +} \ No newline at end of file diff --git a/public/js/ff/tags/show.js b/public/js/ff/tags/show.js index e7142191a9..0cf25a8a26 100644 --- a/public/js/ff/tags/show.js +++ b/public/js/ff/tags/show.js @@ -6,6 +6,8 @@ * See the LICENSE file for details. */ +/** global: zoomLevel, latitude, longitude, google, doPlaceMarker */ + /* Some vars as prep for the map: */ diff --git a/public/js/ff/transactions/list.js b/public/js/ff/transactions/list.js index 39cc72fc9a..d3fdd80214 100644 --- a/public/js/ff/transactions/list.js +++ b/public/js/ff/transactions/list.js @@ -41,7 +41,14 @@ function goToMassEdit() { var checkedArray = getCheckboxes(); // go to specially crafted URL: - window.location.href = 'transactions/mass/edit/' + checkedArray; + var bases = document.getElementsByTagName('base'); + var baseHref = null; + + if (bases.length > 0) { + baseHref = bases[0].href; + } + + window.location.href = baseHref + '/transactions/mass/edit/' + checkedArray; return false; } @@ -50,7 +57,13 @@ function goToMassDelete() { var checkedArray = getCheckboxes(); // go to specially crafted URL: - window.location.href = 'transactions/mass/delete/' + checkedArray; + var bases = document.getElementsByTagName('base'); + var baseHref = null; + + if (bases.length > 0) { + baseHref = bases[0].href; + } + window.location.href = baseHref + '/transactions/mass/delete/' + checkedArray; return false; } diff --git a/public/js/ff/transactions/single/common.js b/public/js/ff/transactions/single/common.js index 0fd23dd6ac..d8ecc8d30c 100644 --- a/public/js/ff/transactions/single/common.js +++ b/public/js/ff/transactions/single/common.js @@ -6,6 +6,10 @@ * See the LICENSE file for details. */ +/** global: Modernizr, accountInfo, currencyInfo, accountInfo, transferInstructions, what */ + +var countConversions = 0; + $(document).ready(function () { "use strict"; setCommonAutocomplete(); @@ -73,7 +77,6 @@ function selectsForeignCurrency() { var nativeCurrencyId = parseInt(accountInfo[selectedAccountId].preferredCurrency); if (foreignCurrencyId !== nativeCurrencyId) { - console.log('User has selected currency #' + foreignCurrencyId + ' and this is different from native currency #' + nativeCurrencyId); // the input where the native amount is entered gets the symbol for the native currency: $('.non-selectable-currency-symbol').text(currencyInfo[nativeCurrencyId].symbol); @@ -90,7 +93,6 @@ function selectsForeignCurrency() { } if (foreignCurrencyId === nativeCurrencyId) { - console.log('User has selected currency #' + foreignCurrencyId + ' and this is equal to native currency #' + nativeCurrencyId + ' (phew).'); $('#exchange_rate_instruction_holder').hide(); $('#native_amount_holder').hide(); } @@ -110,7 +112,6 @@ function convertForeignToNative() { var date = $('#ffInput_date').val(); var amount = $('#ffInput_amount').val(); var uri = 'json/rate/' + foreignCurrencyCode + '/' + nativeCurrencyCode + '/' + date + '?amount=' + amount; - console.log('Will grab ' + uri); $.get(uri).done(updateNativeAmount); } @@ -119,8 +120,12 @@ function convertForeignToNative() { * @param data */ function updateNativeAmount(data) { - console.log('Returned data:'); - console.log(data); + // if native amount is already filled in, even though we do this for the first time: + // don't overrule it. + if (countConversions === 0 && $('#ffInput_native_amount').val().length > 0) { + countConversions++; + return; + } $('#ffInput_native_amount').val(data.amount); } @@ -158,12 +163,10 @@ function validateCurrencyForTransfer() { $('#source_amount_holder').show().find('.non-selectable-currency-symbol').text(sourceSymbol); if (sourceCurrency === destinationCurrency) { - console.log('Both accounts accept #' + sourceCurrency); $('#destination_amount_holder').hide(); $('#amount_holder').hide(); return; } - console.log('Source accepts #' + sourceCurrency + ', destination #' + destinationCurrency); $('#ffInput_exchange_rate_instruction').text(getTransferExchangeInstructions()); $('#exchange_rate_instruction_holder').show(); $('input[name="source_amount"]').val($('input[name="amount"]').val()); @@ -191,7 +194,6 @@ function convertSourceToDestination() { var amount = $('#ffInput_source_amount').val(); $('#ffInput_amount').val(amount); var uri = 'json/rate/' + sourceCurrencyCode + '/' + destinationCurrencyCode + '/' + date + '?amount=' + amount; - console.log('Will grab ' + uri); $.get(uri).done(updateDestinationAmount); } @@ -200,7 +202,5 @@ function convertSourceToDestination() { * @param data */ function updateDestinationAmount(data) { - console.log('Returned data:'); - console.log(data); $('#ffInput_destination_amount').val(data.amount); } \ No newline at end of file diff --git a/public/js/ff/transactions/single/create.js b/public/js/ff/transactions/single/create.js index 56109f855c..08d2f9988d 100644 --- a/public/js/ff/transactions/single/create.js +++ b/public/js/ff/transactions/single/create.js @@ -6,7 +6,7 @@ * See the LICENSE file for details. */ -/** global: what,Modernizr, title, breadcrumbs, middleCrumbName, button, piggiesLength, txt, middleCrumbUrl,exchangeRateInstructions */ +/** global: currencyInfo, accountInfo, what,Modernizr, title, breadcrumbs, middleCrumbName, button, piggiesLength, txt, middleCrumbUrl,exchangeRateInstructions, convertForeignToNative, convertSourceToDestination, selectsForeignCurrency, accountInfo */ $(document).ready(function () { "use strict"; @@ -23,7 +23,6 @@ $(document).ready(function () { updateForm(); updateLayout(); updateDescription(); - updateNativeCurrency(); // when user changes source account or destination, native currency may be different. @@ -60,16 +59,21 @@ function getExchangeInstructions() { * There is an input that shows the currency symbol that is native to the selected * acccount. So when the user changes the selected account, the native currency is updated: */ -function updateNativeCurrency() { - var newAccountId = getAccountId(); - var nativeCurrencyId = accountInfo[newAccountId].preferredCurrency; - - console.log('User selected account #' + newAccountId + '. Native currency is #' + nativeCurrencyId); +function updateNativeCurrency(useAccountCurrency) { + var nativeCurrencyId; + if (useAccountCurrency) { + var newAccountId = getAccountId(); + nativeCurrencyId = accountInfo[newAccountId].preferredCurrency; + } + if (!useAccountCurrency) { + nativeCurrencyId = overruleCurrency; + } $('.currency-option[data-id="' + nativeCurrencyId + '"]').click(); $('[data-toggle="dropdown"]').parent().removeClass('open'); - //$('select[name="source_account_id"]').focus(); - + if (what !== 'transfer') { + $('select[name="source_account_id"]').focus(); + } validateCurrencyForTransfer(); } @@ -105,6 +109,7 @@ function updateForm() { var srcName = $('#ffInput_source_account_name'); switch (what) { + case 'withdrawal': // show source_id and dest_name document.getElementById('source_account_id_holder').style.display = 'block'; @@ -176,8 +181,12 @@ function updateForm() { } document.getElementById('piggy_bank_id_holder').style.display = showPiggies; break; + default: + break; } - updateNativeCurrency(); + // get instructions all the time. + updateNativeCurrency(useAccountCurrency); + selectsForeignCurrency(); } /** @@ -230,4 +239,5 @@ function getAccountId() { if (what === "deposit" || what === "transfer") { return $('select[name="destination_account_id"]').val(); } + return undefined; } diff --git a/public/js/ff/transactions/single/edit.js b/public/js/ff/transactions/single/edit.js index 17f2d0a5c0..4bafe1b1c7 100644 --- a/public/js/ff/transactions/single/edit.js +++ b/public/js/ff/transactions/single/edit.js @@ -8,7 +8,7 @@ * See the LICENSE file for details. */ -/** global: what, Modernizr */ +/** global: what, Modernizr, selectsForeignCurrency, convertForeignToNative, validateCurrencyForTransfer, convertSourceToDestination, journalData, journal, accountInfo, exchangeRateInstructions, currencyInfo */ $(document).ready(function () { "use strict"; @@ -20,8 +20,8 @@ $(document).ready(function () { $('#ffInput_amount').on('change', convertForeignToNative); // respond to transfer changes: - $('#ffInput_source_account_id').on('change',validateCurrencyForTransfer); - $('#ffInput_destination_account_id').on('change',validateCurrencyForTransfer); + $('#ffInput_source_account_id').on('change', validateCurrencyForTransfer); + $('#ffInput_destination_account_id').on('change', validateCurrencyForTransfer); // convert source currency to destination currency (slightly different routine for transfers) $('#ffInput_source_amount').on('change', convertSourceToDestination); @@ -32,17 +32,16 @@ $(document).ready(function () { */ function updateInitialPage() { - console.log('Native currency is #' + journalData.native_currency.id + ' and (foreign) currency id is #' + journalData.currency.id); - if (journal.transaction_type.type === "Transfer") { $('#native_amount_holder').hide(); $('#amount_holder').hide(); - if (journalData.native_currency.id === journalData.currency.id) { + + if (journalData.native_currency.id === journalData.destination_currency.id) { $('#exchange_rate_instruction_holder').hide(); $('#destination_amount_holder').hide(); } - if (journalData.native_currency.id !== journalData.currency.id) { + if (journalData.native_currency.id !== journalData.destination_currency.id) { $('#exchange_rate_instruction_holder').show().find('p').text(getTransferExchangeInstructions()); } @@ -65,7 +64,6 @@ function updateInitialPage() { } - /** * Get accountID based on some meta info. */ @@ -78,6 +76,7 @@ function getAccountId() { } alert('Cannot handle ' + journal.transaction_type.type); + return undefined; } /** diff --git a/public/js/ff/transactions/split/edit.js b/public/js/ff/transactions/split/edit.js index a91c0a3422..d9e7e22ed9 100644 --- a/public/js/ff/transactions/split/edit.js +++ b/public/js/ff/transactions/split/edit.js @@ -16,8 +16,8 @@ var descriptions = {}; $(document).ready(function () { "use strict"; - $('.btn-do-split').click(cloneRow); - $('.remove-current-split').click(removeRow); + $('.btn-do-split').click(cloneDivRow); + $('.remove-current-split').click(removeDivRow); $.getJSON('json/expense-accounts').done(function (data) { destAccounts = data; @@ -67,28 +67,36 @@ $(document).ready(function () { } }); - -function removeRow(e) { +/** + * New and cool + * @param e + * @returns {boolean} + */ +function removeDivRow(e) { "use strict"; - var rows = $('table.split-table tbody tr'); + var rows = $('div.split_row'); if (rows.length === 1) { return false; } var row = $(e.target); var index = row.data('split'); - $('table.split-table tbody tr[data-split="' + index + '"]').remove(); + $('div.split_row[data-split="' + index + '"]').remove(); - resetSplits(); + resetDivSplits(); return false; } -function cloneRow() { +/** + * New and cool + * @returns {boolean} + */ +function cloneDivRow() { "use strict"; - var source = $('.table.split-table tbody tr').last().clone(); - var count = $('.split-table tbody tr').length + 1; + var source = $('div.split_row').last().clone(); + var count = $('div.split_row').length + 1; source.removeClass('initial-row'); source.find('.count').text('#' + count); @@ -107,26 +115,37 @@ function cloneRow() { source.find('input[name$="description]"]').typeahead({source: descriptions}); } - $('.split-table tbody').append(source); + $('div.split_row_holder').append(source); // remove original click things, add them again: - $('.remove-current-split').unbind('click').click(removeRow); - + $('.remove-current-split').unbind('click').click(removeDivRow); calculateSum(); - resetSplits(); + resetDivSplits(); return false; } -function resetSplits() { +/** + * New and hip + */ +function resetDivSplits() { "use strict"; // loop rows, reset numbers: // update the row split number: - $.each($('table.split-table tbody tr'), function (i, v) { + $.each($('div.split_row'), function (i, v) { var row = $(v); row.attr('data-split', i); + + // add or remove class with bg thing + if (i % 2 === 0) { + row.removeClass('bg-gray-light'); + } + if (i % 2 === 1) { + row.addClass('bg-gray-light'); + } + }); // loop each remove button, update the index @@ -166,11 +185,31 @@ function resetSplits() { var input = $(v); input.attr('name', 'transactions[' + i + '][amount]'); }); + + // ends with ][foreign_amount] + $.each($('input[name$="][foreign_amount]"]'), function (i, v) { + var input = $(v); + input.attr('name', 'transactions[' + i + '][foreign_amount]'); + }); + + // ends with ][transaction_currency_id] + $.each($('input[name$="][transaction_currency_id]"]'), function (i, v) { + var input = $(v); + input.attr('name', 'transactions[' + i + '][transaction_currency_id]'); + }); + + // ends with ][foreign_currency_id] + $.each($('input[name$="][foreign_currency_id]"]'), function (i, v) { + var input = $(v); + input.attr('name', 'transactions[' + i + '][foreign_currency_id]'); + }); + // ends with ][budget_id] $.each($('select[name$="][budget_id]"]'), function (i, v) { var input = $(v); input.attr('name', 'transactions[' + i + '][budget_id]'); }); + // ends with ][category] $.each($('input[name$="][category]"]'), function (i, v) { var input = $(v); @@ -178,6 +217,7 @@ function resetSplits() { }); } + function calculateSum() { "use strict"; var sum = 0; diff --git a/public/js/lib/Chart.bundle.min.js b/public/js/lib/Chart.bundle.min.js index 4621b008fc..a7c0cf9757 100644 --- a/public/js/lib/Chart.bundle.min.js +++ b/public/js/lib/Chart.bundle.min.js @@ -1,16 +1,16 @@ /*! * Chart.js * http://chartjs.org/ - * Version: 2.5.0 + * Version: 2.6.0 * * Copyright 2017 Nick Downie * Released under the MIT license * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md */ -!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Chart=t()}}(function(){var t;return function t(e,n,i){function a(o,s){if(!n[o]){if(!e[o]){var l="function"==typeof require&&require;if(!s&&l)return l(o,!0);if(r)return r(o,!0);var u=new Error("Cannot find module '"+o+"'");throw u.code="MODULE_NOT_FOUND",u}var d=n[o]={exports:{}};e[o][0].call(d.exports,function(t){var n=e[o][1][t];return a(n?n:t)},d,d.exports,t,e,n,i)}return n[o].exports}for(var r="function"==typeof require&&require,o=0;on?(e+.05)/(n+.05):(n+.05)/(e+.05)},level:function(t){var e=this.contrast(t);return e>=7.1?"AAA":e>=4.5?"AA":""},dark:function(){var t=this.values.rgb,e=(299*t[0]+587*t[1]+114*t[2])/1e3;return e<128},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;e<3;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){var e=this.values.hsl;return e[2]+=e[2]*t,this.setValues("hsl",e),this},darken:function(t){var e=this.values.hsl;return e[2]-=e[2]*t,this.setValues("hsl",e),this},saturate:function(t){var e=this.values.hsl;return e[1]+=e[1]*t,this.setValues("hsl",e),this},desaturate:function(t){var e=this.values.hsl;return e[1]-=e[1]*t,this.setValues("hsl",e),this},whiten:function(t){var e=this.values.hwb;return e[1]+=e[1]*t,this.setValues("hwb",e),this},blacken:function(t){var e=this.values.hwb;return e[2]+=e[2]*t,this.setValues("hwb",e),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){var e=this.values.alpha;return this.setValues("alpha",e-e*t),this},opaquer:function(t){var e=this.values.alpha;return this.setValues("alpha",e+e*t),this},rotate:function(t){var e=this.values.hsl,n=(e[0]+t)%360;return e[0]=n<0?360+n:n,this.setValues("hsl",e),this},mix:function(t,e){var n=this,i=t,a=void 0===e?.5:e,r=2*a-1,o=n.alpha()-i.alpha(),s=((r*o===-1?r:(r+o)/(1+r*o))+1)/2,l=1-s;return this.rgb(s*n.red()+l*i.red(),s*n.green()+l*i.green(),s*n.blue()+l*i.blue()).alpha(n.alpha()*a+i.alpha()*(1-a))},toJSON:function(){return this.rgb()},clone:function(){var t,e,n=new r,i=this.values,a=n.values;for(var o in i)i.hasOwnProperty(o)&&(t=i[o],e={}.toString.call(t),"[object Array]"===e?a[o]=t.slice(0):"[object Number]"===e?a[o]=t:console.error("unexpected color value:",t));return n}},r.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},r.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},r.prototype.getValues=function(t){for(var e=this.values,n={},i=0;i.04045?Math.pow((e+.055)/1.055,2.4):e/12.92,n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92,i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92;var a=.4124*e+.3576*n+.1805*i,r=.2126*e+.7152*n+.0722*i,o=.0193*e+.1192*n+.9505*i;return[100*a,100*r,100*o]}function d(t){var e,n,i,a=u(t),r=a[0],o=a[1],s=a[2];return r/=95.047,o/=100,s/=108.883,r=r>.008856?Math.pow(r,1/3):7.787*r+16/116,o=o>.008856?Math.pow(o,1/3):7.787*o+16/116,s=s>.008856?Math.pow(s,1/3):7.787*s+16/116,e=116*o-16,n=500*(r-o),i=200*(o-s),[e,n,i]}function c(t){return Y(d(t))}function h(t){var e,n,i,a,r,o=t[0]/360,s=t[1]/100,l=t[2]/100;if(0==s)return r=255*l,[r,r,r];n=l<.5?l*(1+s):l+s-l*s,e=2*l-n,a=[0,0,0];for(var u=0;u<3;u++)i=o+1/3*-(u-1),i<0&&i++,i>1&&i--,r=6*i<1?e+6*(n-e)*i:2*i<1?n:3*i<2?e+(n-e)*(2/3-i)*6:e,a[u]=255*r;return a}function f(t){var e,n,i=t[0],a=t[1]/100,r=t[2]/100;return 0===r?[0,0,0]:(r*=2,a*=r<=1?r:2-r,n=(r+a)/2,e=2*a/(r+a),[i,100*e,100*n])}function m(t){return o(h(t))}function p(t){return s(h(t))}function v(t){return l(h(t))}function y(t){var e=t[0]/60,n=t[1]/100,i=t[2]/100,a=Math.floor(e)%6,r=e-Math.floor(e),o=255*i*(1-n),s=255*i*(1-n*r),l=255*i*(1-n*(1-r)),i=255*i;switch(a){case 0:return[i,l,o];case 1:return[s,i,o];case 2:return[o,i,l];case 3:return[o,s,i];case 4:return[l,o,i];case 5:return[i,o,s]}}function x(t){var e,n,i=t[0],a=t[1]/100,r=t[2]/100;return n=(2-a)*r,e=a*r,e/=n<=1?n:2-n,e=e||0,n/=2,[i,100*e,100*n]}function k(t){return o(y(t))}function _(t){return s(y(t))}function w(t){return l(y(t))}function S(t){var e,n,i,a,o=t[0]/360,s=t[1]/100,l=t[2]/100,u=s+l;switch(u>1&&(s/=u,l/=u),e=Math.floor(6*o),n=1-l,i=6*o-e,0!=(1&e)&&(i=1-i),a=s+i*(n-s),e){default:case 6:case 0:r=n,g=a,b=s;break;case 1:r=a,g=n,b=s;break;case 2:r=s,g=n,b=a;break;case 3:r=s,g=a,b=n;break;case 4:r=a,g=s,b=n;break;case 5:r=n,g=s,b=a}return[255*r,255*g,255*b]}function M(t){return i(S(t))}function D(t){return a(S(t))}function C(t){return s(S(t))}function T(t){return l(S(t))}function P(t){var e,n,i,a=t[0]/100,r=t[1]/100,o=t[2]/100,s=t[3]/100;return e=1-Math.min(1,a*(1-s)+s),n=1-Math.min(1,r*(1-s)+s),i=1-Math.min(1,o*(1-s)+s),[255*e,255*n,255*i]}function I(t){return i(P(t))}function A(t){return a(P(t))}function F(t){return o(P(t))}function O(t){return l(P(t))}function R(t){var e,n,i,a=t[0]/100,r=t[1]/100,o=t[2]/100;return e=3.2406*a+r*-1.5372+o*-.4986,n=a*-.9689+1.8758*r+.0415*o,i=.0557*a+r*-.204+1.057*o,e=e>.0031308?1.055*Math.pow(e,1/2.4)-.055:e*=12.92,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:n*=12.92,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:i*=12.92,e=Math.min(Math.max(0,e),1),n=Math.min(Math.max(0,n),1),i=Math.min(Math.max(0,i),1),[255*e,255*n,255*i]}function L(t){var e,n,i,a=t[0],r=t[1],o=t[2];return a/=95.047,r/=100,o/=108.883,a=a>.008856?Math.pow(a,1/3):7.787*a+16/116,r=r>.008856?Math.pow(r,1/3):7.787*r+16/116,o=o>.008856?Math.pow(o,1/3):7.787*o+16/116,e=116*r-16,n=500*(a-r),i=200*(r-o),[e,n,i]}function V(t){return Y(L(t))}function W(t){var e,n,i,a,r=t[0],o=t[1],s=t[2];return r<=8?(n=100*r/903.3,a=7.787*(n/100)+16/116):(n=100*Math.pow((r+16)/116,3),a=Math.pow(n/100,1/3)),e=e/95.047<=.008856?e=95.047*(o/500+a-16/116)/7.787:95.047*Math.pow(o/500+a,3),i=i/108.883<=.008859?i=108.883*(a-s/200-16/116)/7.787:108.883*Math.pow(a-s/200,3),[e,n,i]}function Y(t){var e,n,i,a=t[0],r=t[1],o=t[2];return e=Math.atan2(o,r),n=360*e/2/Math.PI,n<0&&(n+=360),i=Math.sqrt(r*r+o*o),[a,i,n]}function B(t){return R(W(t))}function z(t){var e,n,i,a=t[0],r=t[1],o=t[2];return i=o/360*2*Math.PI,e=r*Math.cos(i),n=r*Math.sin(i),[a,e,n]}function N(t){return W(z(t))}function H(t){return B(z(t))}function E(t){return J[t]}function U(t){return i(E(t))}function j(t){return a(E(t))}function G(t){return o(E(t))}function q(t){return s(E(t))}function Z(t){return d(E(t))}function X(t){return u(E(t))}e.exports={rgb2hsl:i,rgb2hsv:a,rgb2hwb:o,rgb2cmyk:s,rgb2keyword:l,rgb2xyz:u,rgb2lab:d,rgb2lch:c,hsl2rgb:h,hsl2hsv:f,hsl2hwb:m,hsl2cmyk:p,hsl2keyword:v,hsv2rgb:y,hsv2hsl:x,hsv2hwb:k,hsv2cmyk:_,hsv2keyword:w,hwb2rgb:S,hwb2hsl:M,hwb2hsv:D,hwb2cmyk:C,hwb2keyword:T,cmyk2rgb:P,cmyk2hsl:I,cmyk2hsv:A,cmyk2hwb:F,cmyk2keyword:O,keyword2rgb:E,keyword2hsl:U,keyword2hsv:j,keyword2hwb:G,keyword2cmyk:q,keyword2lab:Z,keyword2xyz:X,xyz2rgb:R,xyz2lab:L,xyz2lch:V,lab2xyz:W,lab2rgb:B,lab2lch:Y,lch2lab:z,lch2xyz:N,lch2rgb:H};var J={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},K={};for(var Q in J)K[JSON.stringify(J[Q])]=Q},{}],4:[function(t,e,n){var i=t(3),a=function(){return new u};for(var r in i){a[r+"Raw"]=function(t){return function(e){return"number"==typeof e&&(e=Array.prototype.slice.call(arguments)),i[t](e)}}(r);var o=/(\w+)2(\w+)/.exec(r),s=o[1],l=o[2];a[s]=a[s]||{},a[s][l]=a[r]=function(t){return function(e){"number"==typeof e&&(e=Array.prototype.slice.call(arguments));var n=i[t](e);if("string"==typeof n||void 0===n)return n;for(var a=0;a0)for(n in xi)i=xi[n],a=e[i],v(a)||(t[i]=a);return t}function y(e){b(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),ki===!1&&(ki=!0,t.updateOffset(this),ki=!1)}function x(t){return t instanceof y||null!=t&&null!=t._isAMomentObject}function k(t){return t<0?Math.ceil(t)||0:Math.floor(t)}function _(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=k(e)),n}function w(t,e,n){var i,a=Math.min(t.length,e.length),r=Math.abs(t.length-e.length),o=0;for(i=0;i0?"future":"past"];return C(n)?n(e):n.replace(/%s/i,e)}function W(t,e){var n=t.toLowerCase();Fi[n]=Fi[n+"s"]=Fi[e]=t}function Y(t){return"string"==typeof t?Fi[t]||Fi[t.toLowerCase()]:void 0}function B(t){var e,n,i={};for(n in t)d(t,n)&&(e=Y(n),e&&(i[e]=t[n]));return i}function z(t,e){Oi[t]=e}function N(t){var e=[];for(var n in t)e.push({unit:n,priority:Oi[n]});return e.sort(function(t,e){return t.priority-e.priority}),e}function H(e,n){return function(i){return null!=i?(U(this,e,i),t.updateOffset(this,n),this):E(this,e)}}function E(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function U(t,e,n){t.isValid()&&t._d["set"+(t._isUTC?"UTC":"")+e](n)}function j(t){return t=Y(t),C(this[t])?this[t]():this}function G(t,e){if("object"==typeof t){t=B(t);for(var n=N(t),i=0;i=0;return(r?n?"+":"":"-")+Math.pow(10,Math.max(0,a)).toString().substr(1)+i}function Z(t,e,n,i){var a=i;"string"==typeof i&&(a=function(){return this[i]()}),t&&(Wi[t]=a),e&&(Wi[e[0]]=function(){return q(a.apply(this,arguments),e[1],e[2])}),n&&(Wi[n]=function(){return this.localeData().ordinal(a.apply(this,arguments),t)})}function X(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function J(t){var e,n,i=t.match(Ri);for(e=0,n=i.length;e=0&&Li.test(t);)t=t.replace(Li,n),Li.lastIndex=0,i-=1;return t}function $(t,e,n){ea[t]=C(e)?e:function(t,i){return t&&n?n:e}}function tt(t,e){return d(ea,t)?ea[t](e._strict,e._locale):new RegExp(et(t))}function et(t){return nt(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,n,i,a){return e||n||i||a}))}function nt(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function it(t,e){var n,i=e;for("string"==typeof t&&(t=[t]),s(e)&&(i=function(t,n){n[e]=_(t)}),n=0;n=0&&isFinite(s.getFullYear())&&s.setFullYear(t),s}function kt(t){var e=new Date(Date.UTC.apply(null,arguments));return t<100&&t>=0&&isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t),e}function _t(t,e,n){var i=7+e-n,a=(7+kt(t,0,i).getUTCDay()-e)%7;return-a+i-1}function wt(t,e,n,i,a){var r,o,s=(7+n-i)%7,l=_t(t,i,a),u=1+7*(e-1)+s+l;return u<=0?(r=t-1,o=vt(r)+u):u>vt(t)?(r=t+1,o=u-vt(t)):(r=t,o=u),{year:r,dayOfYear:o}}function St(t,e,n){var i,a,r=_t(t.year(),e,n),o=Math.floor((t.dayOfYear()-r-1)/7)+1;return o<1?(a=t.year()-1,i=o+Mt(a,e,n)):o>Mt(t.year(),e,n)?(i=o-Mt(t.year(),e,n),a=t.year()+1):(a=t.year(),i=o),{week:i,year:a}}function Mt(t,e,n){var i=_t(t,e,n),a=_t(t+1,e,n);return(vt(t)-i+a)/7}function Dt(t){return St(t,this._week.dow,this._week.doy).week}function Ct(){return this._week.dow}function Tt(){return this._week.doy}function Pt(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function It(t){var e=St(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")}function At(t,e){return"string"!=typeof t?t:isNaN(t)?(t=e.weekdaysParse(t),"number"==typeof t?t:null):parseInt(t,10)}function Ft(t,e){return"string"==typeof t?e.weekdaysParse(t)%7||7:isNaN(t)?null:t}function Ot(t,e){return t?a(this._weekdays)?this._weekdays[t.day()]:this._weekdays[this._weekdays.isFormat.test(e)?"format":"standalone"][t.day()]:this._weekdays}function Rt(t){return t?this._weekdaysShort[t.day()]:this._weekdaysShort}function Lt(t){return t?this._weekdaysMin[t.day()]:this._weekdaysMin}function Vt(t,e,n){var i,a,r,o=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],i=0;i<7;++i)r=h([2e3,1]).day(i),this._minWeekdaysParse[i]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[i]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[i]=this.weekdays(r,"").toLocaleLowerCase();return n?"dddd"===e?(a=ha.call(this._weekdaysParse,o),a!==-1?a:null):"ddd"===e?(a=ha.call(this._shortWeekdaysParse,o),a!==-1?a:null):(a=ha.call(this._minWeekdaysParse,o),a!==-1?a:null):"dddd"===e?(a=ha.call(this._weekdaysParse,o),a!==-1?a:(a=ha.call(this._shortWeekdaysParse,o),a!==-1?a:(a=ha.call(this._minWeekdaysParse,o),a!==-1?a:null))):"ddd"===e?(a=ha.call(this._shortWeekdaysParse,o),a!==-1?a:(a=ha.call(this._weekdaysParse,o),a!==-1?a:(a=ha.call(this._minWeekdaysParse,o),a!==-1?a:null))):(a=ha.call(this._minWeekdaysParse,o),a!==-1?a:(a=ha.call(this._weekdaysParse,o),a!==-1?a:(a=ha.call(this._shortWeekdaysParse,o),a!==-1?a:null)))}function Wt(t,e,n){var i,a,r;if(this._weekdaysParseExact)return Vt.call(this,t,e,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),i=0;i<7;i++){if(a=h([2e3,1]).day(i),n&&!this._fullWeekdaysParse[i]&&(this._fullWeekdaysParse[i]=new RegExp("^"+this.weekdays(a,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[i]=new RegExp("^"+this.weekdaysShort(a,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[i]=new RegExp("^"+this.weekdaysMin(a,"").replace(".",".?")+"$","i")),this._weekdaysParse[i]||(r="^"+this.weekdays(a,"")+"|^"+this.weekdaysShort(a,"")+"|^"+this.weekdaysMin(a,""),this._weekdaysParse[i]=new RegExp(r.replace(".",""),"i")),n&&"dddd"===e&&this._fullWeekdaysParse[i].test(t))return i;if(n&&"ddd"===e&&this._shortWeekdaysParse[i].test(t))return i;if(n&&"dd"===e&&this._minWeekdaysParse[i].test(t))return i;if(!n&&this._weekdaysParse[i].test(t))return i}}function Yt(t){if(!this.isValid())return null!=t?this:NaN;var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=At(t,this.localeData()),this.add(t-e,"d")):e}function Bt(t){if(!this.isValid())return null!=t?this:NaN;var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")}function zt(t){if(!this.isValid())return null!=t?this:NaN;if(null!=t){var e=Ft(t,this.localeData());return this.day(this.day()%7?e:e-7)}return this.day()||7}function Nt(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Ut.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(d(this,"_weekdaysRegex")||(this._weekdaysRegex=wa),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)}function Ht(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Ut.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(d(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=Sa),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Et(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Ut.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(d(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Ma),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Ut(){function t(t,e){return e.length-t.length}var e,n,i,a,r,o=[],s=[],l=[],u=[];for(e=0;e<7;e++)n=h([2e3,1]).day(e),i=this.weekdaysMin(n,""),a=this.weekdaysShort(n,""),r=this.weekdays(n,""),o.push(i),s.push(a),l.push(r),u.push(i),u.push(a),u.push(r);for(o.sort(t),s.sort(t),l.sort(t),u.sort(t),e=0;e<7;e++)s[e]=nt(s[e]),l[e]=nt(l[e]),u[e]=nt(u[e]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+o.join("|")+")","i")}function jt(){return this.hours()%12||12}function Gt(){return this.hours()||24}function qt(t,e){Z(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function Zt(t,e){return e._meridiemParse}function Xt(t){return"p"===(t+"").toLowerCase().charAt(0)}function Jt(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"}function Kt(t){return t?t.toLowerCase().replace("_","-"):t}function Qt(t){for(var e,n,i,a,r=0;r0;){if(i=$t(a.slice(0,e).join("-")))return i;if(n&&n.length>=e&&w(a,n,!0)>=e-1)break;e--}r++}return null}function $t(t){var i=null;if(!Ia[t]&&"undefined"!=typeof n&&n&&n.exports)try{i=Da._abbr,e("./locale/"+t),te(i)}catch(t){}return Ia[t]}function te(t,e){var n;return t&&(n=v(e)?ie(t):ee(t,e),n&&(Da=n)),Da._abbr}function ee(t,e){if(null!==e){var n=Pa;if(e.abbr=t,null!=Ia[t])D("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),n=Ia[t]._config;else if(null!=e.parentLocale){if(null==Ia[e.parentLocale])return Aa[e.parentLocale]||(Aa[e.parentLocale]=[]),Aa[e.parentLocale].push({name:t,config:e}),null;n=Ia[e.parentLocale]._config}return Ia[t]=new I(P(n,e)),Aa[t]&&Aa[t].forEach(function(t){ee(t.name,t.config)}),te(t),Ia[t]}return delete Ia[t],null}function ne(t,e){if(null!=e){var n,i=Pa;null!=Ia[t]&&(i=Ia[t]._config),e=P(i,e),n=new I(e),n.parentLocale=Ia[t],Ia[t]=n,te(t)}else null!=Ia[t]&&(null!=Ia[t].parentLocale?Ia[t]=Ia[t].parentLocale:null!=Ia[t]&&delete Ia[t]);return Ia[t]}function ie(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return Da;if(!a(t)){if(e=$t(t))return e;t=[t]}return Qt(t)}function ae(){return Mi(Ia)}function re(t){var e,n=t._a;return n&&g(t).overflow===-2&&(e=n[aa]<0||n[aa]>11?aa:n[ra]<1||n[ra]>ot(n[ia],n[aa])?ra:n[oa]<0||n[oa]>24||24===n[oa]&&(0!==n[sa]||0!==n[la]||0!==n[ua])?oa:n[sa]<0||n[sa]>59?sa:n[la]<0||n[la]>59?la:n[ua]<0||n[ua]>999?ua:-1,g(t)._overflowDayOfYear&&(era)&&(e=ra),g(t)._overflowWeeks&&e===-1&&(e=da),g(t)._overflowWeekday&&e===-1&&(e=ca),g(t).overflow=e),t}function oe(t){var e,n,i,a,r,o,s=t._i,l=Fa.exec(s)||Oa.exec(s);if(l){for(g(t).iso=!0,e=0,n=La.length;evt(a)&&(g(t)._overflowDayOfYear=!0),n=kt(a,0,t._dayOfYear),t._a[aa]=n.getUTCMonth(),t._a[ra]=n.getUTCDate()),e=0;e<3&&null==t._a[e];++e)t._a[e]=r[e]=i[e];for(;e<7;e++)t._a[e]=r[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[oa]&&0===t._a[sa]&&0===t._a[la]&&0===t._a[ua]&&(t._nextDay=!0,t._a[oa]=0),t._d=(t._useUTC?kt:xt).apply(null,r),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[oa]=24)}}function ce(t){var e,n,i,a,r,o,s,l;if(e=t._w,null!=e.GG||null!=e.W||null!=e.E)r=1,o=4,n=le(e.GG,t._a[ia],St(xe(),1,4).year),i=le(e.W,1),a=le(e.E,1),(a<1||a>7)&&(l=!0);else{r=t._locale._week.dow,o=t._locale._week.doy;var u=St(xe(),r,o);n=le(e.gg,t._a[ia],u.year),i=le(e.w,u.week),null!=e.d?(a=e.d,(a<0||a>6)&&(l=!0)):null!=e.e?(a=e.e+r,(e.e<0||e.e>6)&&(l=!0)):a=r}i<1||i>Mt(n,r,o)?g(t)._overflowWeeks=!0:null!=l?g(t)._overflowWeekday=!0:(s=wt(n,i,a,r,o),t._a[ia]=s.year,t._dayOfYear=s.dayOfYear)}function he(e){if(e._f===t.ISO_8601)return void oe(e);e._a=[],g(e).empty=!0;var n,i,a,r,o,s=""+e._i,l=s.length,u=0;for(a=Q(e._f,e._locale).match(Ri)||[],n=0;n0&&g(e).unusedInput.push(o),s=s.slice(s.indexOf(i)+i.length),u+=i.length),Wi[r]?(i?g(e).empty=!1:g(e).unusedTokens.push(r),rt(r,i,e)):e._strict&&!i&&g(e).unusedTokens.push(r);g(e).charsLeftOver=l-u,s.length>0&&g(e).unusedInput.push(s),e._a[oa]<=12&&g(e).bigHour===!0&&e._a[oa]>0&&(g(e).bigHour=void 0),g(e).parsedDateParts=e._a.slice(0),g(e).meridiem=e._meridiem,e._a[oa]=fe(e._locale,e._a[oa],e._meridiem),de(e),re(e)}function fe(t,e,n){var i;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?(i=t.isPM(n),i&&e<12&&(e+=12),i||12!==e||(e=0),e):e}function ge(t){var e,n,i,a,r;if(0===t._f.length)return g(t).invalidFormat=!0,void(t._d=new Date(NaN));for(a=0;athis.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Ye(){if(!v(this._isDSTShifted))return this._isDSTShifted;var t={};if(b(t,this),t=ve(t),t._a){var e=t._isUTC?h(t._a):xe(t._a);this._isDSTShifted=this.isValid()&&w(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Be(){return!!this.isValid()&&!this._isUTC}function ze(){return!!this.isValid()&&this._isUTC}function Ne(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}function He(t,e){var n,i,a,r=t,o=null;return Me(t)?r={ms:t._milliseconds,d:t._days,M:t._months}:s(t)?(r={},e?r[e]=t:r.milliseconds=t):(o=Ha.exec(t))?(n="-"===o[1]?-1:1,r={y:0,d:_(o[ra])*n,h:_(o[oa])*n,m:_(o[sa])*n,s:_(o[la])*n,ms:_(De(1e3*o[ua]))*n}):(o=Ea.exec(t))?(n="-"===o[1]?-1:1,r={y:Ee(o[2],n),M:Ee(o[3],n),w:Ee(o[4],n),d:Ee(o[5],n),h:Ee(o[6],n),m:Ee(o[7],n),s:Ee(o[8],n)}):null==r?r={}:"object"==typeof r&&("from"in r||"to"in r)&&(a=je(xe(r.from),xe(r.to)),r={},r.ms=a.milliseconds,r.M=a.months),i=new Se(r),Me(t)&&d(t,"_locale")&&(i._locale=t._locale),i}function Ee(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Ue(t,e){var n={milliseconds:0,months:0};return n.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(n.months,"M").isAfter(e)&&--n.months,n.milliseconds=+e-+t.clone().add(n.months,"M"),n}function je(t,e){var n;return t.isValid()&&e.isValid()?(e=Pe(e,t),t.isBefore(e)?n=Ue(t,e):(n=Ue(e,t),n.milliseconds=-n.milliseconds,n.months=-n.months),n):{milliseconds:0,months:0}}function Ge(t,e){return function(n,i){var a,r;return null===i||isNaN(+i)||(D(e,"moment()."+e+"(period, number) is deprecated. Please use moment()."+e+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),r=n,n=i,i=r),n="string"==typeof n?+n:n,a=He(n,i),qe(this,a,t),this}}function qe(e,n,i,a){var r=n._milliseconds,o=De(n._days),s=De(n._months);e.isValid()&&(a=null==a||a,r&&e._d.setTime(e._d.valueOf()+r*i),o&&U(e,"Date",E(e,"Date")+o*i),s&&ct(e,E(e,"Month")+s*i),a&&t.updateOffset(e,o||s))}function Ze(t,e){var n=t.diff(e,"days",!0);return n<-6?"sameElse":n<-1?"lastWeek":n<0?"lastDay":n<1?"sameDay":n<2?"nextDay":n<7?"nextWeek":"sameElse"}function Xe(e,n){var i=e||xe(),a=Pe(i,this).startOf("day"),r=t.calendarFormat(this,a)||"sameElse",o=n&&(C(n[r])?n[r].call(this,i):n[r]);return this.format(o||this.localeData().calendar(r,this,xe(i)))}function Je(){return new y(this)}function Ke(t,e){var n=x(t)?t:xe(t);return!(!this.isValid()||!n.isValid())&&(e=Y(v(e)?"millisecond":e),"millisecond"===e?this.valueOf()>n.valueOf():n.valueOf()r&&(e=r),Rn.call(this,t,e,n,i,a))}function Rn(t,e,n,i,a){var r=wt(t,e,n,i,a),o=kt(r.year,0,r.dayOfYear);return this.year(o.getUTCFullYear()),this.month(o.getUTCMonth()),this.date(o.getUTCDate()),this}function Ln(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)}function Vn(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")}function Wn(t,e){e[ua]=_(1e3*("0."+t))}function Yn(){return this._isUTC?"UTC":""}function Bn(){return this._isUTC?"Coordinated Universal Time":""}function zn(t){return xe(1e3*t)}function Nn(){return xe.apply(null,arguments).parseZone()}function Hn(t){return t}function En(t,e,n,i){var a=ie(),r=h().set(i,e);return a[n](r,t)}function Un(t,e,n){if(s(t)&&(e=t,t=void 0),t=t||"",null!=e)return En(t,e,n,"month");var i,a=[];for(i=0;i<12;i++)a[i]=En(t,i,n,"month");return a}function jn(t,e,n,i){"boolean"==typeof t?(s(e)&&(n=e,e=void 0),e=e||""):(e=t,n=e,t=!1,s(e)&&(n=e,e=void 0),e=e||"");var a=ie(),r=t?a._week.dow:0;if(null!=n)return En(e,(n+r)%7,i,"day");var o,l=[];for(o=0;o<7;o++)l[o]=En(e,(o+r)%7,i,"day");return l}function Gn(t,e){return Un(t,e,"months")}function qn(t,e){return Un(t,e,"monthsShort")}function Zn(t,e,n){return jn(t,e,n,"weekdays")}function Xn(t,e,n){return jn(t,e,n,"weekdaysShort")}function Jn(t,e,n){return jn(t,e,n,"weekdaysMin")}function Kn(){var t=this._data;return this._milliseconds=tr(this._milliseconds),this._days=tr(this._days),this._months=tr(this._months),t.milliseconds=tr(t.milliseconds),t.seconds=tr(t.seconds),t.minutes=tr(t.minutes),t.hours=tr(t.hours),t.months=tr(t.months),t.years=tr(t.years),this}function Qn(t,e,n,i){var a=He(e,n);return t._milliseconds+=i*a._milliseconds,t._days+=i*a._days,t._months+=i*a._months,t._bubble()}function $n(t,e){return Qn(this,t,e,1)}function ti(t,e){return Qn(this,t,e,-1)}function ei(t){return t<0?Math.floor(t):Math.ceil(t)}function ni(){var t,e,n,i,a,r=this._milliseconds,o=this._days,s=this._months,l=this._data;return r>=0&&o>=0&&s>=0||r<=0&&o<=0&&s<=0||(r+=864e5*ei(ai(s)+o),o=0,s=0),l.milliseconds=r%1e3,t=k(r/1e3),l.seconds=t%60,e=k(t/60),l.minutes=e%60,n=k(e/60),l.hours=n%24,o+=k(n/24),a=k(ii(o)),s+=a,o-=ei(ai(a)),i=k(s/12),s%=12,l.days=o,l.months=s,l.years=i,this}function ii(t){return 4800*t/146097}function ai(t){return 146097*t/4800}function ri(t){var e,n,i=this._milliseconds;if(t=Y(t),"month"===t||"year"===t)return e=this._days+i/864e5,n=this._months+ii(e),"month"===t?n:n/12;switch(e=this._days+Math.round(ai(this._months)),t){case"week":return e/7+i/6048e5;case"day":return e+i/864e5;case"hour":return 24*e+i/36e5;case"minute":return 1440*e+i/6e4;case"second":return 86400*e+i/1e3;case"millisecond":return Math.floor(864e5*e)+i;default:throw new Error("Unknown unit "+t)}}function oi(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*_(this._months/12)}function si(t){return function(){return this.as(t)}}function li(t){return t=Y(t),this[t+"s"]()}function ui(t){return function(){return this._data[t]}}function di(){return k(this.days()/7)}function ci(t,e,n,i,a){return a.relativeTime(e||1,!!n,t,i)}function hi(t,e,n){var i=He(t).abs(),a=pr(i.as("s")),r=pr(i.as("m")),o=pr(i.as("h")),s=pr(i.as("d")),l=pr(i.as("M")),u=pr(i.as("y")),d=a0,d[4]=n,ci.apply(null,d)}function fi(t){return void 0===t?pr:"function"==typeof t&&(pr=t,!0)}function gi(t,e){return void 0!==vr[t]&&(void 0===e?vr[t]:(vr[t]=e,!0))}function mi(t){var e=this.localeData(),n=hi(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)}function pi(){var t,e,n,i=br(this._milliseconds)/1e3,a=br(this._days),r=br(this._months);t=k(i/60),e=k(t/60),i%=60,t%=60,n=k(r/12),r%=12;var o=n,s=r,l=a,u=e,d=t,c=i,h=this.asSeconds();return h?(h<0?"-":"")+"P"+(o?o+"Y":"")+(s?s+"M":"")+(l?l+"D":"")+(u||d||c?"T":"")+(u?u+"H":"")+(d?d+"M":"")+(c?c+"S":""):"P0D"}var vi,bi;bi=Array.prototype.some?Array.prototype.some:function(t){for(var e=Object(this),n=e.length>>>0,i=0;i68?1900:2e3)};var ba=H("FullYear",!0);Z("w",["ww",2],"wo","week"),Z("W",["WW",2],"Wo","isoWeek"),W("week","w"),W("isoWeek","W"),z("week",5),z("isoWeek",5),$("w",Ei),$("ww",Ei,Bi),$("W",Ei),$("WW",Ei,Bi),at(["w","ww","W","WW"],function(t,e,n,i){e[i.substr(0,1)]=_(t)});var ya={dow:0,doy:6};Z("d",0,"do","day"),Z("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),Z("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),Z("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),Z("e",0,0,"weekday"),Z("E",0,0,"isoWeekday"),W("day","d"),W("weekday","e"),W("isoWeekday","E"),z("day",11),z("weekday",11),z("isoWeekday",11),$("d",Ei),$("e",Ei),$("E",Ei),$("dd",function(t,e){return e.weekdaysMinRegex(t)}),$("ddd",function(t,e){return e.weekdaysShortRegex(t)}),$("dddd",function(t,e){return e.weekdaysRegex(t)}),at(["dd","ddd","dddd"],function(t,e,n,i){var a=n._locale.weekdaysParse(t,i,n._strict);null!=a?e.d=a:g(n).invalidWeekday=t}),at(["d","e","E"],function(t,e,n,i){e[i]=_(t)});var xa="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),ka="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),_a="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),wa=ta,Sa=ta,Ma=ta;Z("H",["HH",2],0,"hour"),Z("h",["hh",2],0,jt),Z("k",["kk",2],0,Gt),Z("hmm",0,0,function(){return""+jt.apply(this)+q(this.minutes(),2)}),Z("hmmss",0,0,function(){return""+jt.apply(this)+q(this.minutes(),2)+q(this.seconds(),2)}),Z("Hmm",0,0,function(){return""+this.hours()+q(this.minutes(),2)}),Z("Hmmss",0,0,function(){return""+this.hours()+q(this.minutes(),2)+q(this.seconds(),2)}),qt("a",!0),qt("A",!1),W("hour","h"),z("hour",13),$("a",Zt),$("A",Zt),$("H",Ei),$("h",Ei),$("HH",Ei,Bi),$("hh",Ei,Bi),$("hmm",Ui),$("hmmss",ji),$("Hmm",Ui),$("Hmmss",ji),it(["H","HH"],oa),it(["a","A"],function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t}),it(["h","hh"],function(t,e,n){e[oa]=_(t),g(n).bigHour=!0}),it("hmm",function(t,e,n){var i=t.length-2;e[oa]=_(t.substr(0,i)),e[sa]=_(t.substr(i)),g(n).bigHour=!0}),it("hmmss",function(t,e,n){var i=t.length-4,a=t.length-2;e[oa]=_(t.substr(0,i)),e[sa]=_(t.substr(i,2)),e[la]=_(t.substr(a)),g(n).bigHour=!0}),it("Hmm",function(t,e,n){var i=t.length-2;e[oa]=_(t.substr(0,i)),e[sa]=_(t.substr(i))}),it("Hmmss",function(t,e,n){var i=t.length-4,a=t.length-2;e[oa]=_(t.substr(0,i)),e[sa]=_(t.substr(i,2)),e[la]=_(t.substr(a))});var Da,Ca=/[ap]\.?m?\.?/i,Ta=H("Hours",!0),Pa={calendar:Di,longDateFormat:Ci,invalidDate:Ti,ordinal:Pi,ordinalParse:Ii,relativeTime:Ai,months:ga,monthsShort:ma,week:ya,weekdays:xa,weekdaysMin:_a,weekdaysShort:ka,meridiemParse:Ca},Ia={},Aa={},Fa=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Oa=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ra=/Z|[+-]\d\d(?::?\d\d)?/,La=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Va=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Wa=/^\/?Date\((\-?\d+)/i; -t.createFromInputFallback=M("value provided is not in a recognized ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))}),t.ISO_8601=function(){};var Ya=M("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var t=xe.apply(null,arguments);return this.isValid()&&t.isValid()?tthis?this:t:p()}),za=function(){return Date.now?Date.now():+new Date};Ce("Z",":"),Ce("ZZ",""),$("Z",Qi),$("ZZ",Qi),it(["Z","ZZ"],function(t,e,n){n._useUTC=!0,n._tzm=Te(Qi,t)});var Na=/([\+\-]|\d\d)/gi;t.updateOffset=function(){};var Ha=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Ea=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;He.fn=Se.prototype;var Ua=Ge(1,"add"),ja=Ge(-1,"subtract");t.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",t.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Ga=M("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(t){return void 0===t?this.localeData():this.locale(t)});Z(0,["gg",2],0,function(){return this.weekYear()%100}),Z(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Tn("gggg","weekYear"),Tn("ggggg","weekYear"),Tn("GGGG","isoWeekYear"),Tn("GGGGG","isoWeekYear"),W("weekYear","gg"),W("isoWeekYear","GG"),z("weekYear",1),z("isoWeekYear",1),$("G",Ji),$("g",Ji),$("GG",Ei,Bi),$("gg",Ei,Bi),$("GGGG",qi,Ni),$("gggg",qi,Ni),$("GGGGG",Zi,Hi),$("ggggg",Zi,Hi),at(["gggg","ggggg","GGGG","GGGGG"],function(t,e,n,i){e[i.substr(0,2)]=_(t)}),at(["gg","GG"],function(e,n,i,a){n[a]=t.parseTwoDigitYear(e)}),Z("Q",0,"Qo","quarter"),W("quarter","Q"),z("quarter",7),$("Q",Yi),it("Q",function(t,e){e[aa]=3*(_(t)-1)}),Z("D",["DD",2],"Do","date"),W("date","D"),z("date",9),$("D",Ei),$("DD",Ei,Bi),$("Do",function(t,e){return t?e._ordinalParse:e._ordinalParseLenient}),it(["D","DD"],ra),it("Do",function(t,e){e[ra]=_(t.match(Ei)[0],10)});var qa=H("Date",!0);Z("DDD",["DDDD",3],"DDDo","dayOfYear"),W("dayOfYear","DDD"),z("dayOfYear",4),$("DDD",Gi),$("DDDD",zi),it(["DDD","DDDD"],function(t,e,n){n._dayOfYear=_(t)}),Z("m",["mm",2],0,"minute"),W("minute","m"),z("minute",14),$("m",Ei),$("mm",Ei,Bi),it(["m","mm"],sa);var Za=H("Minutes",!1);Z("s",["ss",2],0,"second"),W("second","s"),z("second",15),$("s",Ei),$("ss",Ei,Bi),it(["s","ss"],la);var Xa=H("Seconds",!1);Z("S",0,0,function(){return~~(this.millisecond()/100)}),Z(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),Z(0,["SSS",3],0,"millisecond"),Z(0,["SSSS",4],0,function(){return 10*this.millisecond()}),Z(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),Z(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),Z(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),Z(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),Z(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),W("millisecond","ms"),z("millisecond",16),$("S",Gi,Yi),$("SS",Gi,Bi),$("SSS",Gi,zi);var Ja;for(Ja="SSSS";Ja.length<=9;Ja+="S")$(Ja,Xi);for(Ja="S";Ja.length<=9;Ja+="S")it(Ja,Wn);var Ka=H("Milliseconds",!1);Z("z",0,0,"zoneAbbr"),Z("zz",0,0,"zoneName");var Qa=y.prototype;Qa.add=Ua,Qa.calendar=Xe,Qa.clone=Je,Qa.diff=an,Qa.endOf=vn,Qa.format=un,Qa.from=dn,Qa.fromNow=cn,Qa.to=hn,Qa.toNow=fn,Qa.get=j,Qa.invalidAt=Dn,Qa.isAfter=Ke,Qa.isBefore=Qe,Qa.isBetween=$e,Qa.isSame=tn,Qa.isSameOrAfter=en,Qa.isSameOrBefore=nn,Qa.isValid=Sn,Qa.lang=Ga,Qa.locale=gn,Qa.localeData=mn,Qa.max=Ba,Qa.min=Ya,Qa.parsingFlags=Mn,Qa.set=G,Qa.startOf=pn,Qa.subtract=ja,Qa.toArray=kn,Qa.toObject=_n,Qa.toDate=xn,Qa.toISOString=sn,Qa.inspect=ln,Qa.toJSON=wn,Qa.toString=on,Qa.unix=yn,Qa.valueOf=bn,Qa.creationData=Cn,Qa.year=ba,Qa.isLeapYear=yt,Qa.weekYear=Pn,Qa.isoWeekYear=In,Qa.quarter=Qa.quarters=Ln,Qa.month=ht,Qa.daysInMonth=ft,Qa.week=Qa.weeks=Pt,Qa.isoWeek=Qa.isoWeeks=It,Qa.weeksInYear=Fn,Qa.isoWeeksInYear=An,Qa.date=qa,Qa.day=Qa.days=Yt,Qa.weekday=Bt,Qa.isoWeekday=zt,Qa.dayOfYear=Vn,Qa.hour=Qa.hours=Ta,Qa.minute=Qa.minutes=Za,Qa.second=Qa.seconds=Xa,Qa.millisecond=Qa.milliseconds=Ka,Qa.utcOffset=Ae,Qa.utc=Oe,Qa.local=Re,Qa.parseZone=Le,Qa.hasAlignedHourOffset=Ve,Qa.isDST=We,Qa.isLocal=Be,Qa.isUtcOffset=ze,Qa.isUtc=Ne,Qa.isUTC=Ne,Qa.zoneAbbr=Yn,Qa.zoneName=Bn,Qa.dates=M("dates accessor is deprecated. Use date instead.",qa),Qa.months=M("months accessor is deprecated. Use month instead",ht),Qa.years=M("years accessor is deprecated. Use year instead",ba),Qa.zone=M("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Fe),Qa.isDSTShifted=M("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Ye);var $a=I.prototype;$a.calendar=A,$a.longDateFormat=F,$a.invalidDate=O,$a.ordinal=R,$a.preparse=Hn,$a.postformat=Hn,$a.relativeTime=L,$a.pastFuture=V,$a.set=T,$a.months=st,$a.monthsShort=lt,$a.monthsParse=dt,$a.monthsRegex=mt,$a.monthsShortRegex=gt,$a.week=Dt,$a.firstDayOfYear=Tt,$a.firstDayOfWeek=Ct,$a.weekdays=Ot,$a.weekdaysMin=Lt,$a.weekdaysShort=Rt,$a.weekdaysParse=Wt,$a.weekdaysRegex=Nt,$a.weekdaysShortRegex=Ht,$a.weekdaysMinRegex=Et,$a.isPM=Xt,$a.meridiem=Jt,te("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10,n=1===_(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+n}}),t.lang=M("moment.lang is deprecated. Use moment.locale instead.",te),t.langData=M("moment.langData is deprecated. Use moment.localeData instead.",ie);var tr=Math.abs,er=si("ms"),nr=si("s"),ir=si("m"),ar=si("h"),rr=si("d"),or=si("w"),sr=si("M"),lr=si("y"),ur=ui("milliseconds"),dr=ui("seconds"),cr=ui("minutes"),hr=ui("hours"),fr=ui("days"),gr=ui("months"),mr=ui("years"),pr=Math.round,vr={s:45,m:45,h:22,d:26,M:11},br=Math.abs,yr=Se.prototype;return yr.abs=Kn,yr.add=$n,yr.subtract=ti,yr.as=ri,yr.asMilliseconds=er,yr.asSeconds=nr,yr.asMinutes=ir,yr.asHours=ar,yr.asDays=rr,yr.asWeeks=or,yr.asMonths=sr,yr.asYears=lr,yr.valueOf=oi,yr._bubble=ni,yr.get=li,yr.milliseconds=ur,yr.seconds=dr,yr.minutes=cr,yr.hours=hr,yr.days=fr,yr.weeks=di,yr.months=gr,yr.years=mr,yr.humanize=mi,yr.toISOString=pi,yr.toString=pi,yr.toJSON=pi,yr.locale=gn,yr.localeData=mn,yr.toIsoString=M("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",pi),yr.lang=Ga,Z("X",0,0,"unix"),Z("x",0,0,"valueOf"),$("x",Ji),$("X",$i),it("X",function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))}),it("x",function(t,e,n){n._d=new Date(_(t))}),t.version="2.17.1",i(xe),t.fn=Qa,t.min=_e,t.max=we,t.now=za,t.utc=h,t.unix=zn,t.months=Gn,t.isDate=l,t.locale=te,t.invalid=p,t.duration=He,t.isMoment=x,t.weekdays=Zn,t.parseZone=Nn,t.localeData=ie,t.isDuration=Me,t.monthsShort=qn,t.weekdaysMin=Jn,t.defineLocale=ee,t.updateLocale=ne,t.locales=ae,t.weekdaysShort=Xn,t.normalizeUnits=Y,t.relativeTimeRounding=fi,t.relativeTimeThreshold=gi,t.calendarFormat=Ze,t.prototype=Qa,t})},{}],7:[function(t,e,n){var i=t(28)();t(26)(i),t(42)(i),t(22)(i),t(31)(i),t(25)(i),t(21)(i),t(23)(i),t(24)(i),t(29)(i),t(33)(i),t(34)(i),t(32)(i),t(35)(i),t(30)(i),t(27)(i),t(36)(i),t(37)(i),t(38)(i),t(39)(i),t(40)(i),t(45)(i),t(43)(i),t(44)(i),t(46)(i),t(47)(i),t(48)(i),t(15)(i),t(16)(i),t(17)(i),t(18)(i),t(19)(i),t(20)(i),t(8)(i),t(9)(i),t(10)(i),t(11)(i),t(12)(i),t(13)(i),t(14)(i),window.Chart=e.exports=i},{10:10,11:11,12:12,13:13,14:14,15:15,16:16,17:17,18:18,19:19,20:20,21:21,22:22,23:23,24:24,25:25,26:26,27:27,28:28,29:29,30:30,31:31,32:32,33:33,34:34,35:35,36:36,37:37,38:38,39:39,40:40,42:42,43:43,44:44,45:45,46:46,47:47,48:48,8:8,9:9}],8:[function(t,e,n){"use strict";e.exports=function(t){t.Bar=function(e,n){return n.type="bar",new t(e,n)}}},{}],9:[function(t,e,n){"use strict";e.exports=function(t){t.Bubble=function(e,n){return n.type="bubble",new t(e,n)}}},{}],10:[function(t,e,n){"use strict";e.exports=function(t){t.Doughnut=function(e,n){return n.type="doughnut",new t(e,n)}}},{}],11:[function(t,e,n){"use strict";e.exports=function(t){t.Line=function(e,n){return n.type="line",new t(e,n)}}},{}],12:[function(t,e,n){"use strict";e.exports=function(t){t.PolarArea=function(e,n){return n.type="polarArea",new t(e,n)}}},{}],13:[function(t,e,n){"use strict";e.exports=function(t){t.Radar=function(e,n){return n.type="radar",new t(e,n)}}},{}],14:[function(t,e,n){"use strict";e.exports=function(t){var e={hover:{mode:"single"},scales:{xAxes:[{type:"linear",position:"bottom",id:"x-axis-1"}],yAxes:[{type:"linear",position:"left",id:"y-axis-1"}]},tooltips:{callbacks:{title:function(){return""},label:function(t){return"("+t.xLabel+", "+t.yLabel+")"}}}};t.defaults.scatter=e,t.controllers.scatter=t.controllers.line,t.Scatter=function(e,n){return n.type="scatter",new t(e,n)}}},{}],15:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.bar={hover:{mode:"label"},scales:{xAxes:[{type:"category",categoryPercentage:.8,barPercentage:.9,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}},t.controllers.bar=t.DatasetController.extend({dataElementType:t.elements.Rectangle,initialize:function(e,n){t.DatasetController.prototype.initialize.call(this,e,n);var i=this,a=i.getMeta(),r=i.getDataset();a.stack=r.stack,a.bar=!0},getStackCount:function(){var t=this,n=t.getMeta(),i=t.getScaleForId(n.yAxisID),a=[];return e.each(t.chart.data.datasets,function(e,n){var r=t.chart.getDatasetMeta(n);r.bar&&t.chart.isDatasetVisible(n)&&(i.options.stacked===!1||i.options.stacked===!0&&a.indexOf(r.stack)===-1||void 0===i.options.stacked&&(void 0===r.stack||a.indexOf(r.stack)===-1))&&a.push(r.stack)},t),a.length},update:function(t){var n=this;e.each(n.getMeta().data,function(e,i){n.updateElement(e,i,t)},n)},updateElement:function(t,n,i){var a=this,r=a.getMeta(),o=a.getScaleForId(r.xAxisID),s=a.getScaleForId(r.yAxisID),l=s.getBasePixel(),u=a.chart.options.elements.rectangle,d=t.custom||{},c=a.getDataset();t._xScale=o,t._yScale=s,t._datasetIndex=a.index,t._index=n;var h=a.getRuler(n);t._model={x:a.calculateBarX(n,a.index,h),y:i?l:a.calculateBarY(n,a.index),label:a.chart.data.labels[n],datasetLabel:c.label,horizontal:!1,base:i?l:a.calculateBarBase(a.index,n),width:a.calculateBarWidth(h),backgroundColor:d.backgroundColor?d.backgroundColor:e.getValueAtIndexOrDefault(c.backgroundColor,n,u.backgroundColor),borderSkipped:d.borderSkipped?d.borderSkipped:u.borderSkipped,borderColor:d.borderColor?d.borderColor:e.getValueAtIndexOrDefault(c.borderColor,n,u.borderColor),borderWidth:d.borderWidth?d.borderWidth:e.getValueAtIndexOrDefault(c.borderWidth,n,u.borderWidth)},t.pivot()},calculateBarBase:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.yAxisID),r=a.getBaseValue(),o=r;if(a.options.stacked===!0||void 0===a.options.stacked&&void 0!==i.stack){for(var s=n.chart,l=s.data.datasets,u=Number(l[t].data[e]),d=0;d0&&(t[0].yLabel?n=t[0].yLabel:e.labels.length>0&&t[0].index');var n=t.data,i=n.datasets,a=n.labels;if(i.length)for(var r=0;r'),a[r]&&e.push(a[r]),e.push("
  • ");return e.push(""),e.join("")},legend:{labels:{generateLabels:function(t){var n=t.data;return n.labels.length&&n.datasets.length?n.labels.map(function(i,a){var r=t.getDatasetMeta(0),o=n.datasets[0],s=r.data[a],l=s&&s.custom||{},u=e.getValueAtIndexOrDefault,d=t.options.elements.arc,c=l.backgroundColor?l.backgroundColor:u(o.backgroundColor,a,d.backgroundColor),h=l.borderColor?l.borderColor:u(o.borderColor,a,d.borderColor),f=l.borderWidth?l.borderWidth:u(o.borderWidth,a,d.borderWidth);return{text:i,fillStyle:c,strokeStyle:h,lineWidth:f,hidden:isNaN(o.data[a])||r.data[a].hidden,index:a}}):[]}},onClick:function(t,e){var n,i,a,r=e.index,o=this.chart;for(n=0,i=(o.data.datasets||[]).length;n=Math.PI?-1:g<-Math.PI?1:0);var m=g+f,p={x:Math.cos(g),y:Math.sin(g)},v={x:Math.cos(m),y:Math.sin(m)},b=g<=0&&0<=m||g<=2*Math.PI&&2*Math.PI<=m,y=g<=.5*Math.PI&&.5*Math.PI<=m||g<=2.5*Math.PI&&2.5*Math.PI<=m,x=g<=-Math.PI&&-Math.PI<=m||g<=Math.PI&&Math.PI<=m,k=g<=.5*-Math.PI&&.5*-Math.PI<=m||g<=1.5*Math.PI&&1.5*Math.PI<=m,_=h/100,w={x:x?-1:Math.min(p.x*(p.x<0?1:_),v.x*(v.x<0?1:_)),y:k?-1:Math.min(p.y*(p.y<0?1:_),v.y*(v.y<0?1:_))},S={x:b?1:Math.max(p.x*(p.x>0?1:_),v.x*(v.x>0?1:_)),y:y?1:Math.max(p.y*(p.y>0?1:_),v.y*(v.y>0?1:_))},M={width:.5*(S.x-w.x),height:.5*(S.y-w.y)};u=Math.min(s/M.width,l/M.height),d={x:(S.x+w.x)*-.5,y:(S.y+w.y)*-.5}}i.borderWidth=n.getMaxBorderWidth(c.data),i.outerRadius=Math.max((u-i.borderWidth)/2,0),i.innerRadius=Math.max(h?i.outerRadius/100*h:0,0),i.radiusLength=(i.outerRadius-i.innerRadius)/i.getVisibleDatasetCount(),i.offsetX=d.x*i.outerRadius,i.offsetY=d.y*i.outerRadius,c.total=n.calculateTotal(),n.outerRadius=i.outerRadius-i.radiusLength*n.getRingIndex(n.index),n.innerRadius=Math.max(n.outerRadius-i.radiusLength,0),e.each(c.data,function(e,i){n.updateElement(e,i,t)})},updateElement:function(t,n,i){var a=this,r=a.chart,o=r.chartArea,s=r.options,l=s.animation,u=(o.left+o.right)/2,d=(o.top+o.bottom)/2,c=s.rotation,h=s.rotation,f=a.getDataset(),g=i&&l.animateRotate?0:t.hidden?0:a.calculateCircumference(f.data[n])*(s.circumference/(2*Math.PI)),m=i&&l.animateScale?0:a.innerRadius,p=i&&l.animateScale?0:a.outerRadius,v=e.getValueAtIndexOrDefault;e.extend(t,{_datasetIndex:a.index,_index:n,_model:{x:u+r.offsetX,y:d+r.offsetY,startAngle:c,endAngle:h,circumference:g,outerRadius:p,innerRadius:m,label:v(f.label,n,r.data.labels[n])}});var b=t._model;this.removeHoverStyle(t),i&&l.animateRotate||(0===n?b.startAngle=s.rotation:b.startAngle=a.getMeta().data[n-1]._model.endAngle,b.endAngle=b.startAngle+b.circumference),t.pivot()},removeHoverStyle:function(e){t.DatasetController.prototype.removeHoverStyle.call(this,e,this.chart.options.elements.arc)},calculateTotal:function(){var t,n=this.getDataset(),i=this.getMeta(),a=0;return e.each(i.data,function(e,i){t=n.data[i],isNaN(t)||e.hidden||(a+=Math.abs(t))}),a},calculateCircumference:function(t){var e=this.getMeta().total;return e>0&&!isNaN(t)?2*Math.PI*(t/e):0},getMaxBorderWidth:function(t){for(var e,n,i=0,a=this.index,r=t.length,o=0;oi?e:i,i=n>i?n:i;return i}})}},{}],18:[function(t,e,n){"use strict";e.exports=function(t){function e(t,e){return n.getValueOrDefault(t.showLine,e.showLines)}var n=t.helpers;t.defaults.line={showLines:!0,spanGaps:!1,hover:{mode:"label"},scales:{xAxes:[{type:"category",id:"x-axis-0"}],yAxes:[{type:"linear",id:"y-axis-0"}]}},t.controllers.line=t.DatasetController.extend({datasetElementType:t.elements.Line,dataElementType:t.elements.Point,update:function(t){var i,a,r,o=this,s=o.getMeta(),l=s.dataset,u=s.data||[],d=o.chart.options,c=d.elements.line,h=o.getScaleForId(s.yAxisID),f=o.getDataset(),g=e(f,d);for(g&&(r=l.custom||{},void 0!==f.tension&&void 0===f.lineTension&&(f.lineTension=f.tension),l._scale=h,l._datasetIndex=o.index,l._children=u,l._model={spanGaps:f.spanGaps?f.spanGaps:d.spanGaps,tension:r.tension?r.tension:n.getValueOrDefault(f.lineTension,c.tension),backgroundColor:r.backgroundColor?r.backgroundColor:f.backgroundColor||c.backgroundColor,borderWidth:r.borderWidth?r.borderWidth:f.borderWidth||c.borderWidth,borderColor:r.borderColor?r.borderColor:f.borderColor||c.borderColor,borderCapStyle:r.borderCapStyle?r.borderCapStyle:f.borderCapStyle||c.borderCapStyle,borderDash:r.borderDash?r.borderDash:f.borderDash||c.borderDash,borderDashOffset:r.borderDashOffset?r.borderDashOffset:f.borderDashOffset||c.borderDashOffset,borderJoinStyle:r.borderJoinStyle?r.borderJoinStyle:f.borderJoinStyle||c.borderJoinStyle,fill:r.fill?r.fill:void 0!==f.fill?f.fill:c.fill,steppedLine:r.steppedLine?r.steppedLine:n.getValueOrDefault(f.steppedLine,c.stepped),cubicInterpolationMode:r.cubicInterpolationMode?r.cubicInterpolationMode:n.getValueOrDefault(f.cubicInterpolationMode,c.cubicInterpolationMode),scaleTop:h.top,scaleBottom:h.bottom,scaleZero:h.getBasePixel()},l.pivot()),i=0,a=u.length;i');var n=t.data,i=n.datasets,a=n.labels;if(i.length)for(var r=0;r'),a[r]&&e.push(a[r]),e.push("");return e.push(""),e.join(""); -},legend:{labels:{generateLabels:function(t){var n=t.data;return n.labels.length&&n.datasets.length?n.labels.map(function(i,a){var r=t.getDatasetMeta(0),o=n.datasets[0],s=r.data[a],l=s.custom||{},u=e.getValueAtIndexOrDefault,d=t.options.elements.arc,c=l.backgroundColor?l.backgroundColor:u(o.backgroundColor,a,d.backgroundColor),h=l.borderColor?l.borderColor:u(o.borderColor,a,d.borderColor),f=l.borderWidth?l.borderWidth:u(o.borderWidth,a,d.borderWidth);return{text:i,fillStyle:c,strokeStyle:h,lineWidth:f,hidden:isNaN(o.data[a])||r.data[a].hidden,index:a}}):[]}},onClick:function(t,e){var n,i,a,r=e.index,o=this.chart;for(n=0,i=(o.data.datasets||[]).length;n0&&!isNaN(t)?2*Math.PI/e:0}})}},{}],20:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.radar={aspectRatio:1,scale:{type:"radialLinear"},elements:{line:{tension:0}}},t.controllers.radar=t.DatasetController.extend({datasetElementType:t.elements.Line,dataElementType:t.elements.Point,linkScales:e.noop,update:function(t){var n=this,i=n.getMeta(),a=i.dataset,r=i.data,o=a.custom||{},s=n.getDataset(),l=n.chart.options.elements.line,u=n.chart.scale;void 0!==s.tension&&void 0===s.lineTension&&(s.lineTension=s.tension),e.extend(i.dataset,{_datasetIndex:n.index,_children:r,_loop:!0,_model:{tension:o.tension?o.tension:e.getValueOrDefault(s.lineTension,l.tension),backgroundColor:o.backgroundColor?o.backgroundColor:s.backgroundColor||l.backgroundColor,borderWidth:o.borderWidth?o.borderWidth:s.borderWidth||l.borderWidth,borderColor:o.borderColor?o.borderColor:s.borderColor||l.borderColor,fill:o.fill?o.fill:void 0!==s.fill?s.fill:l.fill,borderCapStyle:o.borderCapStyle?o.borderCapStyle:s.borderCapStyle||l.borderCapStyle,borderDash:o.borderDash?o.borderDash:s.borderDash||l.borderDash,borderDashOffset:o.borderDashOffset?o.borderDashOffset:s.borderDashOffset||l.borderDashOffset,borderJoinStyle:o.borderJoinStyle?o.borderJoinStyle:s.borderJoinStyle||l.borderJoinStyle,scaleTop:u.top,scaleBottom:u.bottom,scaleZero:u.getBasePosition()}}),i.dataset.pivot(),e.each(r,function(e,i){n.updateElement(e,i,t)},n),n.updateBezierControlPoints()},updateElement:function(t,n,i){var a=this,r=t.custom||{},o=a.getDataset(),s=a.chart.scale,l=a.chart.options.elements.point,u=s.getPointPositionForValue(n,o.data[n]);e.extend(t,{_datasetIndex:a.index,_index:n,_scale:s,_model:{x:i?s.xCenter:u.x,y:i?s.yCenter:u.y,tension:r.tension?r.tension:e.getValueOrDefault(o.lineTension,a.chart.options.elements.line.tension),radius:r.radius?r.radius:e.getValueAtIndexOrDefault(o.pointRadius,n,l.radius),backgroundColor:r.backgroundColor?r.backgroundColor:e.getValueAtIndexOrDefault(o.pointBackgroundColor,n,l.backgroundColor),borderColor:r.borderColor?r.borderColor:e.getValueAtIndexOrDefault(o.pointBorderColor,n,l.borderColor),borderWidth:r.borderWidth?r.borderWidth:e.getValueAtIndexOrDefault(o.pointBorderWidth,n,l.borderWidth),pointStyle:r.pointStyle?r.pointStyle:e.getValueAtIndexOrDefault(o.pointStyle,n,l.pointStyle),hitRadius:r.hitRadius?r.hitRadius:e.getValueAtIndexOrDefault(o.hitRadius,n,l.hitRadius)}}),t._model.skip=r.skip?r.skip:isNaN(t._model.x)||isNaN(t._model.y)},updateBezierControlPoints:function(){var t=this.chart.chartArea,n=this.getMeta();e.each(n.data,function(i,a){var r=i._model,o=e.splineCurve(e.previousItem(n.data,a,!0)._model,r,e.nextItem(n.data,a,!0)._model,r.tension);r.controlPointPreviousX=Math.max(Math.min(o.previous.x,t.right),t.left),r.controlPointPreviousY=Math.max(Math.min(o.previous.y,t.bottom),t.top),r.controlPointNextX=Math.max(Math.min(o.next.x,t.right),t.left),r.controlPointNextY=Math.max(Math.min(o.next.y,t.bottom),t.top),i.pivot()})},draw:function(t){var n=this.getMeta(),i=t||1;e.each(n.data,function(t){t.transition(i)}),n.dataset.transition(i).draw(),e.each(n.data,function(t){t.draw()})},setHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t.custom||{},a=t._index,r=t._model;r.radius=i.hoverRadius?i.hoverRadius:e.getValueAtIndexOrDefault(n.pointHoverRadius,a,this.chart.options.elements.point.hoverRadius),r.backgroundColor=i.hoverBackgroundColor?i.hoverBackgroundColor:e.getValueAtIndexOrDefault(n.pointHoverBackgroundColor,a,e.getHoverColor(r.backgroundColor)),r.borderColor=i.hoverBorderColor?i.hoverBorderColor:e.getValueAtIndexOrDefault(n.pointHoverBorderColor,a,e.getHoverColor(r.borderColor)),r.borderWidth=i.hoverBorderWidth?i.hoverBorderWidth:e.getValueAtIndexOrDefault(n.pointHoverBorderWidth,a,r.borderWidth)},removeHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t.custom||{},a=t._index,r=t._model,o=this.chart.options.elements.point;r.radius=i.radius?i.radius:e.getValueAtIndexOrDefault(n.radius,a,o.radius),r.backgroundColor=i.backgroundColor?i.backgroundColor:e.getValueAtIndexOrDefault(n.pointBackgroundColor,a,o.backgroundColor),r.borderColor=i.borderColor?i.borderColor:e.getValueAtIndexOrDefault(n.pointBorderColor,a,o.borderColor),r.borderWidth=i.borderWidth?i.borderWidth:e.getValueAtIndexOrDefault(n.pointBorderWidth,a,o.borderWidth)}})}},{}],21:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.global.animation={duration:1e3,easing:"easeOutQuart",onProgress:e.noop,onComplete:e.noop},t.Animation=t.Element.extend({currentStep:null,numSteps:60,easing:"",render:null,onAnimationProgress:null,onAnimationComplete:null}),t.animationService={frameDuration:17,animations:[],dropFrames:0,request:null,addAnimation:function(t,e,n,i){var a=this;i||(t.animating=!0);for(var r=0;r1&&(n=Math.floor(t.dropFrames),t.dropFrames=t.dropFrames%1);for(var i=0;it.animations[i].animationObject.numSteps&&(t.animations[i].animationObject.currentStep=t.animations[i].animationObject.numSteps),t.animations[i].animationObject.render(t.animations[i].chartInstance,t.animations[i].animationObject),t.animations[i].animationObject.onAnimationProgress&&t.animations[i].animationObject.onAnimationProgress.call&&t.animations[i].animationObject.onAnimationProgress.call(t.animations[i].chartInstance,t.animations[i]),t.animations[i].animationObject.currentStep===t.animations[i].animationObject.numSteps?(t.animations[i].animationObject.onAnimationComplete&&t.animations[i].animationObject.onAnimationComplete.call&&t.animations[i].animationObject.onAnimationComplete.call(t.animations[i].chartInstance,t.animations[i]),t.animations[i].chartInstance.animating=!1,t.animations.splice(i,1)):++i;var a=Date.now(),r=(a-e)/t.frameDuration;t.dropFrames+=r,t.animations.length>0&&t.requestAnimationFrame()}}}},{}],22:[function(t,e,n){"use strict";e.exports=function(t){var e=t.canvasHelpers={};e.drawPoint=function(e,n,i,a,r){var o,s,l,u,d,c;if("object"==typeof n&&(o=n.toString(),"[object HTMLImageElement]"===o||"[object HTMLCanvasElement]"===o))return void e.drawImage(n,a-n.width/2,r-n.height/2);if(!(isNaN(i)||i<=0)){switch(n){default:e.beginPath(),e.arc(a,r,i,0,2*Math.PI),e.closePath(),e.fill();break;case"triangle":e.beginPath(),s=3*i/Math.sqrt(3),d=s*Math.sqrt(3)/2,e.moveTo(a-s/2,r+d/3),e.lineTo(a+s/2,r+d/3),e.lineTo(a,r-2*d/3),e.closePath(),e.fill();break;case"rect":c=1/Math.SQRT2*i,e.beginPath(),e.fillRect(a-c,r-c,2*c,2*c),e.strokeRect(a-c,r-c,2*c,2*c);break;case"rectRounded":var h=i/Math.SQRT2,f=a-h,g=r-h,m=Math.SQRT2*i;t.helpers.drawRoundedRectangle(e,f,g,m,m,i/2),e.fill();break;case"rectRot":c=1/Math.SQRT2*i,e.beginPath(),e.moveTo(a-c,r),e.lineTo(a,r+c),e.lineTo(a+c,r),e.lineTo(a,r-c),e.closePath(),e.fill();break;case"cross":e.beginPath(),e.moveTo(a,r+i),e.lineTo(a,r-i),e.moveTo(a-i,r),e.lineTo(a+i,r),e.closePath();break;case"crossRot":e.beginPath(),l=Math.cos(Math.PI/4)*i,u=Math.sin(Math.PI/4)*i,e.moveTo(a-l,r-u),e.lineTo(a+l,r+u),e.moveTo(a-l,r+u),e.lineTo(a+l,r-u),e.closePath();break;case"star":e.beginPath(),e.moveTo(a,r+i),e.lineTo(a,r-i),e.moveTo(a-i,r),e.lineTo(a+i,r),l=Math.cos(Math.PI/4)*i,u=Math.sin(Math.PI/4)*i,e.moveTo(a-l,r-u),e.lineTo(a+l,r+u),e.moveTo(a-l,r+u),e.lineTo(a+l,r-u),e.closePath();break;case"line":e.beginPath(),e.moveTo(a-i,r),e.lineTo(a+i,r),e.closePath();break;case"dash":e.beginPath(),e.moveTo(a,r),e.lineTo(a+i,r),e.closePath()}e.stroke()}},e.clipArea=function(t,e){t.save(),t.beginPath(),t.rect(e.left,e.top,e.right-e.left,e.bottom-e.top),t.clip()},e.unclipArea=function(t){t.restore()}}},{}],23:[function(t,e,n){"use strict";e.exports=function(t){function e(e){e=e||{};var n=e.data=e.data||{};return n.datasets=n.datasets||[],n.labels=n.labels||[],e.options=i.configMerge(t.defaults.global,t.defaults[e.type],e.options||{}),e}function n(t){var e=t.options;e.scale?t.scale.options=e.scale:e.scales&&e.scales.xAxes.concat(e.scales.yAxes).forEach(function(e){t.scales[e.id].options=e}),t.tooltip._options=e.tooltips}var i=t.helpers,a=t.plugins,r=t.platform;t.types={},t.instances={},t.controllers={},t.Controller=function(n,a,o){var s=this;a=e(a);var l=r.acquireContext(n,a),u=l&&l.canvas,d=u&&u.height,c=u&&u.width;return o.ctx=l,o.canvas=u,o.config=a,o.width=c,o.height=d,o.aspectRatio=d?c/d:null,s.id=i.uid(),s.chart=o,s.config=a,s.options=a.options,s._bufferedRender=!1,t.instances[s.id]=s,Object.defineProperty(s,"data",{get:function(){return s.config.data}}),l&&u?(s.initialize(),s.update(),s):(console.error("Failed to create chart: can't acquire context from the given item"),s)},i.extend(t.Controller.prototype,{initialize:function(){var t=this;return a.notify(t,"beforeInit"),i.retinaScale(t.chart),t.bindEvents(),t.options.responsive&&t.resize(!0),t.ensureScalesHaveIDs(),t.buildScales(),t.initToolTip(),a.notify(t,"afterInit"),t},clear:function(){return i.clear(this.chart),this},stop:function(){return t.animationService.cancelAnimation(this),this},resize:function(t){var e=this,n=e.chart,r=e.options,o=n.canvas,s=r.maintainAspectRatio&&n.aspectRatio||null,l=Math.floor(i.getMaximumWidth(o)),u=Math.floor(s?l/s:i.getMaximumHeight(o));if((n.width!==l||n.height!==u)&&(o.width=n.width=l,o.height=n.height=u,o.style.width=l+"px",o.style.height=u+"px",i.retinaScale(n),!t)){var d={width:l,height:u};a.notify(e,"resize",[d]),e.options.onResize&&e.options.onResize(e,d),e.stop(),e.update(e.options.responsiveAnimationDuration)}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},n=t.scale;i.each(e.xAxes,function(t,e){t.id=t.id||"x-axis-"+e}),i.each(e.yAxes,function(t,e){t.id=t.id||"y-axis-"+e}),n&&(n.id=n.id||"scale")},buildScales:function(){var e=this,n=e.options,a=e.scales={},r=[];n.scales&&(r=r.concat((n.scales.xAxes||[]).map(function(t){return{options:t,dtype:"category"}}),(n.scales.yAxes||[]).map(function(t){return{options:t,dtype:"linear"}}))),n.scale&&r.push({options:n.scale,dtype:"radialLinear",isDefault:!0}),i.each(r,function(n){var r=n.options,o=i.getValueOrDefault(r.type,n.dtype),s=t.scaleService.getScaleConstructor(o);if(s){var l=new s({id:r.id,options:r,ctx:e.chart.ctx,chart:e});a[l.id]=l,n.isDefault&&(e.scale=l)}}),t.scaleService.addScalesToLayout(this)},buildOrUpdateControllers:function(){var e=this,n=[],a=[];if(i.each(e.data.datasets,function(i,r){var o=e.getDatasetMeta(r);o.type||(o.type=i.type||e.config.type),n.push(o.type),o.controller?o.controller.updateIndex(r):(o.controller=new t.controllers[o.type](e,r),a.push(o.controller))},e),n.length>1)for(var r=1;r0||(a.forEach(function(e){delete t[e]}),delete t._chartjs)}}var i=t.helpers,a=["push","pop","shift","splice","unshift"];t.DatasetController=function(t,e){this.initialize(t,e)},i.extend(t.DatasetController.prototype,{datasetElementType:null,dataElementType:null,initialize:function(t,e){var n=this;n.chart=t,n.index=e,n.linkScales(),n.addElements()},updateIndex:function(t){this.index=t},linkScales:function(){var t=this,e=t.getMeta(),n=t.getDataset();null===e.xAxisID&&(e.xAxisID=n.xAxisID||t.chart.options.scales.xAxes[0].id),null===e.yAxisID&&(e.yAxisID=n.yAxisID||t.chart.options.scales.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(t){return this.chart.scales[t]},reset:function(){this.update(!0)},destroy:function(){this._data&&n(this._data,this)},createMetaDataset:function(){var t=this,e=t.datasetElementType;return e&&new e({_chart:t.chart.chart,_datasetIndex:t.index})},createMetaData:function(t){var e=this,n=e.dataElementType;return n&&new n({_chart:e.chart.chart,_datasetIndex:e.index,_index:t})},addElements:function(){var t,e,n=this,i=n.getMeta(),a=n.getDataset().data||[],r=i.data;for(t=0,e=a.length;ti&&t.insertElements(i,a-i)},insertElements:function(t,e){for(var n=0;n=0;a--)e.call(n,t[a],a);else for(a=0;a=i[n].length||!i[n][a].type?i[n].push(r.configMerge(s,e)):e.type&&e.type!==i[n][a].type?i[n][a]=r.configMerge(i[n][a],s,e):i[n][a]=r.configMerge(i[n][a],e)}):(i[n]=[],r.each(e,function(e){var a=r.getValueOrDefault(e.type,"xAxes"===n?"category":"linear");i[n].push(r.configMerge(t.scaleService.getScaleDefaults(a),e))})):i.hasOwnProperty(n)&&"object"==typeof i[n]&&null!==i[n]&&"object"==typeof e?i[n]=r.configMerge(i[n],e):i[n]=e}),i},r.getValueAtIndexOrDefault=function(t,e,n){return void 0===t||null===t?n:r.isArray(t)?e=0;i--){var a=t[i];if(e(a))return a}},r.inherits=function(t){var e=this,n=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return e.apply(this,arguments)},i=function(){this.constructor=n};return i.prototype=e.prototype,n.prototype=new i,n.extend=r.inherits,t&&r.extend(n.prototype,t),n.__super__=e.prototype,n},r.noop=function(){},r.uid=function(){var t=0;return function(){return t++}}(),r.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},r.almostEquals=function(t,e,n){return Math.abs(t-e)t},r.max=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.max(t,e)},Number.NEGATIVE_INFINITY)},r.min=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.min(t,e)},Number.POSITIVE_INFINITY)},r.sign=Math.sign?function(t){return Math.sign(t)}:function(t){return t=+t,0===t||isNaN(t)?t:t>0?1:-1},r.log10=Math.log10?function(t){return Math.log10(t)}:function(t){return Math.log(t)/Math.LN10},r.toRadians=function(t){return t*(Math.PI/180)},r.toDegrees=function(t){return t*(180/Math.PI)},r.getAngleFromPoint=function(t,e){var n=e.x-t.x,i=e.y-t.y,a=Math.sqrt(n*n+i*i),r=Math.atan2(i,n);return r<-.5*Math.PI&&(r+=2*Math.PI),{angle:r,distance:a}},r.distanceBetweenPoints=function(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))},r.aliasPixel=function(t){return t%2===0?0:.5},r.splineCurve=function(t,e,n,i){var a=t.skip?e:t,r=e,o=n.skip?e:n,s=Math.sqrt(Math.pow(r.x-a.x,2)+Math.pow(r.y-a.y,2)),l=Math.sqrt(Math.pow(o.x-r.x,2)+Math.pow(o.y-r.y,2)),u=s/(s+l),d=l/(s+l);u=isNaN(u)?0:u,d=isNaN(d)?0:d;var c=i*u,h=i*d;return{previous:{x:r.x-c*(o.x-a.x),y:r.y-c*(o.y-a.y)},next:{x:r.x+h*(o.x-a.x),y:r.y+h*(o.y-a.y)}}},r.EPSILON=Number.EPSILON||1e-14,r.splineCurveMonotone=function(t){var e,n,i,a,o=(t||[]).map(function(t){return{model:t._model,deltaK:0,mK:0}}),s=o.length;for(e=0;e0?o[e-1]:null,a=e0?o[e-1]:null,a=e=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},r.previousItem=function(t,e,n){return n?e<=0?t[t.length-1]:t[e-1]:e<=0?t[0]:t[e-1]},r.niceNum=function(t,e){var n,i=Math.floor(r.log10(t)),a=t/Math.pow(10,i);return n=e?a<1.5?1:a<3?2:a<7?5:10:a<=1?1:a<=2?2:a<=5?5:10,n*Math.pow(10,i)};var o=r.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===(t/=1)?1:(n||(n=.3),i0?(n=l[0].clientX,i=l[0].clientY):(n=a.clientX,i=a.clientY);var u=parseFloat(r.getStyle(o,"padding-left")),d=parseFloat(r.getStyle(o,"padding-top")),c=parseFloat(r.getStyle(o,"padding-right")),h=parseFloat(r.getStyle(o,"padding-bottom")),f=s.right-s.left-u-c,g=s.bottom-s.top-d-h;return n=Math.round((n-s.left-u)/f*o.width/e.currentDevicePixelRatio),i=Math.round((i-s.top-d)/g*o.height/e.currentDevicePixelRatio),{x:n,y:i}},r.addEvent=function(t,e,n){t.addEventListener?t.addEventListener(e,n):t.attachEvent?t.attachEvent("on"+e,n):t["on"+e]=n},r.removeEvent=function(t,e,n){t.removeEventListener?t.removeEventListener(e,n,!1):t.detachEvent?t.detachEvent("on"+e,n):t["on"+e]=r.noop},r.getConstraintWidth=function(t){return a(t,"max-width","clientWidth")},r.getConstraintHeight=function(t){return a(t,"max-height","clientHeight")},r.getMaximumWidth=function(t){var e=t.parentNode,n=parseInt(r.getStyle(e,"padding-left"),10),i=parseInt(r.getStyle(e,"padding-right"),10),a=e.clientWidth-n-i,o=r.getConstraintWidth(t);return isNaN(o)?a:Math.min(a,o)},r.getMaximumHeight=function(t){var e=t.parentNode,n=parseInt(r.getStyle(e,"padding-top"),10),i=parseInt(r.getStyle(e,"padding-bottom"),10),a=e.clientHeight-n-i,o=r.getConstraintHeight(t);return isNaN(o)?a:Math.min(a,o)},r.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},r.retinaScale=function(t){var e=t.currentDevicePixelRatio=window.devicePixelRatio||1;if(1!==e){var n=t.canvas,i=t.height,a=t.width;n.height=i*e,n.width=a*e,t.ctx.scale(e,e),n.style.height=i+"px",n.style.width=a+"px"}},r.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},r.fontString=function(t,e,n){return e+" "+t+"px "+n},r.longestText=function(t,e,n,i){i=i||{};var a=i.data=i.data||{},o=i.garbageCollect=i.garbageCollect||[];i.font!==e&&(a=i.data={},o=i.garbageCollect=[],i.font=e),t.font=e;var s=0;r.each(n,function(e){void 0!==e&&null!==e&&r.isArray(e)!==!0?s=r.measureText(t,a,o,s,e):r.isArray(e)&&r.each(e,function(e){void 0===e||null===e||r.isArray(e)||(s=r.measureText(t,a,o,s,e))})});var l=o.length/2;if(l>n.length){for(var u=0;ui&&(i=r),i},r.numberOfLabelLines=function(t){var e=1;return r.each(t,function(t){r.isArray(t)&&t.length>e&&(e=t.length)}),e},r.drawRoundedRectangle=function(t,e,n,i,a,r){t.beginPath(),t.moveTo(e+r,n),t.lineTo(e+i-r,n),t.quadraticCurveTo(e+i,n,e+i,n+r),t.lineTo(e+i,n+a-r),t.quadraticCurveTo(e+i,n+a,e+i-r,n+a),t.lineTo(e+r,n+a),t.quadraticCurveTo(e,n+a,e,n+a-r),t.lineTo(e,n+r),t.quadraticCurveTo(e,n,e+r,n),t.closePath()},r.color=function(e){return i?i(e instanceof CanvasGradient?t.defaults.global.defaultColor:e):(console.error("Color.js not found!"),e)},r.isArray=Array.isArray?function(t){return Array.isArray(t)}:function(t){return"[object Array]"===Object.prototype.toString.call(t)},r.arrayEquals=function(t,e){var n,i,a,o;if(!t||!e||t.length!==e.length)return!1;for(n=0,i=t.length;n0&&(s=t.getDatasetMeta(s[0]._datasetIndex).data),s},"x-axis":function(t,e){return r(t,e,!0)},point:function(t,n){var a=e(n,t.chart);return i(t,a)},nearest:function(t,n,i){var r=e(n,t.chart),o=a(t,r,i.intersect);return o.length>1&&o.sort(function(t,e){var n=t.getArea(),i=e.getArea(),a=n-i;return 0===a&&(a=t._datasetIndex-e._datasetIndex),a}),o.slice(0,1)},x:function(t,i,a){var r=e(i,t.chart),o=[],s=!1;return n(t,function(t){t.inXRange(r.x)&&o.push(t),t.inRange(r.x,r.y)&&(s=!0)}),a.intersect&&!s&&(o=[]),o},y:function(t,i,a){var r=e(i,t.chart),o=[],s=!1;return n(t,function(t){t.inYRange(r.y)&&o.push(t),t.inRange(r.x,r.y)&&(s=!0)}),a.intersect&&!s&&(o=[]),o}}}}},{}],28:[function(t,e,n){"use strict";e.exports=function(){var t=function(e,n){return this.controller=new t.Controller(e,n,this),this.controller};return t.defaults={global:{responsive:!0,responsiveAnimationDuration:0,maintainAspectRatio:!0,events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,defaultColor:"rgba(0,0,0,0.1)",defaultFontColor:"#666",defaultFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",defaultFontSize:12,defaultFontStyle:"normal",showLines:!0,elements:{},legendCallback:function(t){var e=[];e.push('
      ');for(var n=0;n'),t.data.datasets[n].label&&e.push(t.data.datasets[n].label),e.push("");return e.push("
    "),e.join("")}}},t.Chart=t,t}},{}],29:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.layoutService={defaults:{},addBox:function(t,e){t.boxes||(t.boxes=[]),t.boxes.push(e)},removeBox:function(t,e){t.boxes&&t.boxes.splice(t.boxes.indexOf(e),1)},update:function(t,n,i){function a(t){var e,n=t.isHorizontal();n?(e=t.update(t.options.fullWidth?y:M,S),D-=e.height):(e=t.update(w,_),M-=e.width),C.push({horizontal:n,minSize:e,box:t})}function r(t){var n=e.findNextWhere(C,function(e){return e.box===t});if(n)if(t.isHorizontal()){var i={left:Math.max(F,T),right:Math.max(O,P),top:0,bottom:0};t.update(t.options.fullWidth?y:M,x/2,i)}else t.update(n.minSize.width,D)}function o(t){var n=e.findNextWhere(C,function(e){return e.box===t}),i={left:0,right:0,top:R,bottom:L};n&&t.update(n.minSize.width,D,i)}function s(t){t.isHorizontal()?(t.left=t.options.fullWidth?d:F,t.right=t.options.fullWidth?n-c:F+M,t.top=N,t.bottom=N+t.height,N=t.bottom):(t.left=z,t.right=z+t.width,t.top=R,t.bottom=R+D,z=t.right)}if(t){var l=t.options.layout,u=l?l.padding:null,d=0,c=0,h=0,f=0;isNaN(u)?(d=u.left||0,c=u.right||0,h=u.top||0,f=u.bottom||0):(d=u,c=u,h=u,f=u);var g=e.where(t.boxes,function(t){return"left"===t.options.position}),m=e.where(t.boxes,function(t){return"right"===t.options.position}),p=e.where(t.boxes,function(t){return"top"===t.options.position}),v=e.where(t.boxes,function(t){return"bottom"===t.options.position}),b=e.where(t.boxes,function(t){return"chartArea"===t.options.position});p.sort(function(t,e){return(e.options.fullWidth?1:0)-(t.options.fullWidth?1:0)}),v.sort(function(t,e){return(t.options.fullWidth?1:0)-(e.options.fullWidth?1:0)});var y=n-d-c,x=i-h-f,k=y/2,_=x/2,w=(n-k)/(g.length+m.length),S=(i-_)/(p.length+v.length),M=y,D=x,C=[];e.each(g.concat(m,p,v),a);var T=0,P=0,I=0,A=0;e.each(p.concat(v),function(t){if(t.getPadding){var e=t.getPadding();T=Math.max(T,e.left),P=Math.max(P,e.right)}}),e.each(g.concat(m),function(t){if(t.getPadding){var e=t.getPadding();I=Math.max(I,e.top),A=Math.max(A,e.bottom)}});var F=d,O=c,R=h,L=f;e.each(g.concat(m),r),e.each(g,function(t){F+=t.width}),e.each(m,function(t){O+=t.width}),e.each(p.concat(v),r),e.each(p,function(t){R+=t.height}),e.each(v,function(t){L+=t.height}),e.each(g.concat(m),o),F=d,O=c,R=h,L=f,e.each(g,function(t){F+=t.width}),e.each(m,function(t){O+=t.width}),e.each(p,function(t){R+=t.height}),e.each(v,function(t){L+=t.height});var V=Math.max(T-F,0);F+=V,O+=Math.max(P-O,0);var W=Math.max(I-R,0);R+=W,L+=Math.max(A-L,0);var Y=i-R-L,B=n-F-O;B===M&&Y===D||(e.each(g,function(t){t.height=Y}),e.each(m,function(t){t.height=Y}),e.each(p,function(t){t.options.fullWidth||(t.width=B)}),e.each(v,function(t){t.options.fullWidth||(t.width=B)}),D=Y,M=B);var z=d+V,N=h+W;e.each(g.concat(p),s),z+=M,N+=D,e.each(m,s),e.each(v,s),t.chartArea={left:F,top:R,right:F+M,bottom:R+D},e.each(b,function(e){e.left=t.chartArea.left,e.top=t.chartArea.top,e.right=t.chartArea.right,e.bottom=t.chartArea.bottom,e.update(M,D)})}}}}},{}],30:[function(t,e,n){"use strict";e.exports=function(t){function e(t,e){return t.usePointStyle?e*Math.SQRT2:t.boxWidth}function n(e,n){var i=new t.Legend({ctx:e.chart.ctx,options:n,chart:e});e.legend=i,t.layoutService.addBox(e,i)}var i=t.helpers,a=i.noop;t.defaults.global.legend={display:!0,position:"top",fullWidth:!0,reverse:!1,onClick:function(t,e){var n=e.datasetIndex,i=this.chart,a=i.getDatasetMeta(n);a.hidden=null===a.hidden?!i.data.datasets[n].hidden:null,i.update()},onHover:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data;return i.isArray(e.datasets)?e.datasets.map(function(e,n){return{text:e.label,fillStyle:i.isArray(e.backgroundColor)?e.backgroundColor[0]:e.backgroundColor,hidden:!t.isDatasetVisible(n),lineCap:e.borderCapStyle,lineDash:e.borderDash,lineDashOffset:e.borderDashOffset,lineJoin:e.borderJoinStyle,lineWidth:e.borderWidth,strokeStyle:e.borderColor,pointStyle:e.pointStyle,datasetIndex:n}},this):[]}}},t.Legend=t.Element.extend({initialize:function(t){i.extend(this,t),this.legendHitBoxes=[],this.doughnutMode=!1},beforeUpdate:a,update:function(t,e,n){var i=this;return i.beforeUpdate(),i.maxWidth=t,i.maxHeight=e,i.margins=n,i.beforeSetDimensions(),i.setDimensions(),i.afterSetDimensions(),i.beforeBuildLabels(),i.buildLabels(),i.afterBuildLabels(),i.beforeFit(),i.fit(),i.afterFit(),i.afterUpdate(),i.minSize},afterUpdate:a,beforeSetDimensions:a,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:a,beforeBuildLabels:a,buildLabels:function(){var t=this,e=t.options.labels,n=e.generateLabels.call(t,t.chart);e.filter&&(n=n.filter(function(n){return e.filter(n,t.chart.data)})),t.options.reverse&&n.reverse(),t.legendItems=n},afterBuildLabels:a,beforeFit:a,fit:function(){var n=this,a=n.options,r=a.labels,o=a.display,s=n.ctx,l=t.defaults.global,u=i.getValueOrDefault,d=u(r.fontSize,l.defaultFontSize),c=u(r.fontStyle,l.defaultFontStyle),h=u(r.fontFamily,l.defaultFontFamily),f=i.fontString(d,c,h),g=n.legendHitBoxes=[],m=n.minSize,p=n.isHorizontal();if(p?(m.width=n.maxWidth,m.height=o?10:0):(m.width=o?10:0,m.height=n.maxHeight),o)if(s.font=f,p){var v=n.lineWidths=[0],b=n.legendItems.length?d+r.padding:0;s.textAlign="left",s.textBaseline="top",i.each(n.legendItems,function(t,i){var a=e(r,d),o=a+d/2+s.measureText(t.text).width;v[v.length-1]+o+r.padding>=n.width&&(b+=d+r.padding,v[v.length]=n.left),g[i]={left:0,top:0,width:o,height:d},v[v.length-1]+=o+r.padding}),m.height+=b}else{var y=r.padding,x=n.columnWidths=[],k=r.padding,_=0,w=0,S=d+y;i.each(n.legendItems,function(t,n){var i=e(r,d),a=i+d/2+s.measureText(t.text).width;w+S>m.height&&(k+=_+r.padding,x.push(_),_=0,w=0),_=Math.max(_,a),w+=S,g[n]={left:0,top:0,width:a,height:d}}),k+=_,x.push(_),m.width+=k}n.width=m.width,n.height=m.height},afterFit:a,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var n=this,a=n.options,r=a.labels,o=t.defaults.global,s=o.elements.line,l=n.width,u=n.lineWidths;if(a.display){var d,c=n.ctx,h=i.getValueOrDefault,f=h(r.fontColor,o.defaultFontColor),g=h(r.fontSize,o.defaultFontSize),m=h(r.fontStyle,o.defaultFontStyle),p=h(r.fontFamily,o.defaultFontFamily),v=i.fontString(g,m,p);c.textAlign="left",c.textBaseline="top",c.lineWidth=.5,c.strokeStyle=f,c.fillStyle=f,c.font=v;var b=e(r,g),y=n.legendHitBoxes,x=function(e,n,i){if(!(isNaN(b)||b<=0)){c.save(),c.fillStyle=h(i.fillStyle,o.defaultColor),c.lineCap=h(i.lineCap,s.borderCapStyle),c.lineDashOffset=h(i.lineDashOffset,s.borderDashOffset),c.lineJoin=h(i.lineJoin,s.borderJoinStyle),c.lineWidth=h(i.lineWidth,s.borderWidth),c.strokeStyle=h(i.strokeStyle,o.defaultColor);var r=0===h(i.lineWidth,s.borderWidth);if(c.setLineDash&&c.setLineDash(h(i.lineDash,s.borderDash)),a.labels&&a.labels.usePointStyle){var l=g*Math.SQRT2/2,u=l/Math.SQRT2,d=e+u,f=n+u;t.canvasHelpers.drawPoint(c,i.pointStyle,l,d,f)}else r||c.strokeRect(e,n,b,g),c.fillRect(e,n,b,g);c.restore()}},k=function(t,e,n,i){c.fillText(n.text,b+g/2+t,e),n.hidden&&(c.beginPath(),c.lineWidth=2,c.moveTo(b+g/2+t,e+g/2),c.lineTo(b+g/2+t+i,e+g/2),c.stroke())},_=n.isHorizontal();d=_?{x:n.left+(l-u[0])/2,y:n.top+r.padding,line:0}:{x:n.left+r.padding,y:n.top+r.padding,line:0};var w=g+r.padding;i.each(n.legendItems,function(t,e){var i=c.measureText(t.text).width,a=b+g/2+i,o=d.x,s=d.y;_?o+a>=l&&(s=d.y+=w,d.line++,o=d.x=n.left+(l-u[d.line])/2):s+w>n.bottom&&(o=d.x=o+n.columnWidths[d.line]+r.padding,s=d.y=n.top+r.padding,d.line++),x(o,s,t),y[e].left=o,y[e].top=s,k(o,s,t,i),_?d.x+=a+r.padding:d.y+=w})}},handleEvent:function(t){var e=this,n=e.options,i="mouseup"===t.type?"click":t.type,a=!1;if("mousemove"===i){if(!n.onHover)return}else{if("click"!==i)return;if(!n.onClick)return}var r=t.x,o=t.y;if(r>=e.left&&r<=e.right&&o>=e.top&&o<=e.bottom)for(var s=e.legendHitBoxes,l=0;l=u.left&&r<=u.left+u.width&&o>=u.top&&o<=u.top+u.height){if("click"===i){n.onClick.call(e,t.native,e.legendItems[l]),a=!0;break}if("mousemove"===i){n.onHover.call(e,t.native,e.legendItems[l]),a=!0;break}}}return a}}),t.plugins.register({beforeInit:function(t){var e=t.options.legend;e&&n(t,e)},beforeUpdate:function(e){var a=e.options.legend;a?(a=i.configMerge(t.defaults.global.legend,a),e.legend?e.legend.options=a:n(e,a)):(t.layoutService.removeBox(e,e.legend),delete e.legend)},afterEvent:function(t,e){var n=t.legend;n&&n.handleEvent(e)}})}},{}],31:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.global.plugins={},t.plugins={_plugins:[],_cacheId:0,register:function(t){var e=this._plugins;[].concat(t).forEach(function(t){e.indexOf(t)===-1&&e.push(t)}),this._cacheId++},unregister:function(t){var e=this._plugins;[].concat(t).forEach(function(t){var n=e.indexOf(t);n!==-1&&e.splice(n,1)}),this._cacheId++},clear:function(){this._plugins=[],this._cacheId++},count:function(){return this._plugins.length},getAll:function(){return this._plugins},notify:function(t,e,n){var i,a,r,o,s,l=this.descriptors(t),u=l.length;for(i=0;ic&&ot.maxHeight){o--;break}o++,d=s*u}t.labelRotation=o},afterCalculateTickRotation:function(){i.callCallback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){i.callCallback(this.options.beforeFit,[this])},fit:function(){var t=this,a=t.minSize={width:0,height:0},r=t.options,o=r.ticks,s=r.scaleLabel,l=r.gridLines,u=r.display,d=t.isHorizontal(),c=n(o),h=1.5*n(s).size,f=r.gridLines.tickMarkLength;if(d?a.width=t.isFullWidth()?t.maxWidth-t.margins.left-t.margins.right:t.maxWidth:a.width=u&&l.drawTicks?f:0,d?a.height=u&&l.drawTicks?f:0:a.height=t.maxHeight,s.display&&u&&(d?a.height+=h:a.width+=h),o.display&&u){var g=i.longestText(t.ctx,c.font,t.ticks,t.longestTextCache),m=i.numberOfLabelLines(t.ticks),p=.5*c.size;if(d){t.longestLabelWidth=g;var v=i.toRadians(t.labelRotation),b=Math.cos(v),y=Math.sin(v),x=y*g+c.size*m+p*m;a.height=Math.min(t.maxHeight,a.height+x),t.ctx.font=c.font;var k=t.ticks[0],_=e(t.ctx,k,c.font),w=t.ticks[t.ticks.length-1],S=e(t.ctx,w,c.font);0!==t.labelRotation?(t.paddingLeft="bottom"===r.position?b*_+3:b*p+3,t.paddingRight="bottom"===r.position?b*p+3:b*S+3):(t.paddingLeft=_/2+3,t.paddingRight=S/2+3)}else o.mirror?g=0:g+=t.options.ticks.padding,a.width+=g,t.paddingTop=c.size/2,t.paddingBottom=c.size/2}t.handleMargins(),t.width=a.width,t.height=a.height},handleMargins:function(){var t=this;t.margins&&(t.paddingLeft=Math.max(t.paddingLeft-t.margins.left,0),t.paddingTop=Math.max(t.paddingTop-t.margins.top,0),t.paddingRight=Math.max(t.paddingRight-t.margins.right,0),t.paddingBottom=Math.max(t.paddingBottom-t.margins.bottom,0))},afterFit:function(){i.callCallback(this.options.afterFit,[this])},isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(t){return null===t||"undefined"==typeof t?NaN:"number"!=typeof t||isFinite(t)?"object"==typeof t?t instanceof Date||t.isValid?t:this.getRightValue(this.isHorizontal()?t.x:t.y):t:NaN},getLabelForIndex:i.noop,getPixelForValue:i.noop,getValueForPixel:i.noop,getPixelForTick:function(t,e){var n=this;if(n.isHorizontal()){var i=n.width-(n.paddingLeft+n.paddingRight),a=i/Math.max(n.ticks.length-(n.options.gridLines.offsetGridLines?0:1),1),r=a*t+n.paddingLeft;e&&(r+=a/2);var o=n.left+Math.round(r);return o+=n.isFullWidth()?n.margins.left:0}var s=n.height-(n.paddingTop+n.paddingBottom);return n.top+t*(s/(n.ticks.length-1))},getPixelForDecimal:function(t){var e=this;if(e.isHorizontal()){var n=e.width-(e.paddingLeft+e.paddingRight),i=n*t+e.paddingLeft,a=e.left+Math.round(i);return a+=e.isFullWidth()?e.margins.left:0}return e.top+t*e.height},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var t=this,e=t.min,n=t.max;return t.beginAtZero?0:e<0&&n<0?n:e>0&&n>0?e:0},draw:function(e){var a=this,r=a.options;if(r.display){var o,s,l=a.ctx,u=t.defaults.global,d=r.ticks,c=r.gridLines,h=r.scaleLabel,f=0!==a.labelRotation,g=d.autoSkip,m=a.isHorizontal();d.maxTicksLimit&&(s=d.maxTicksLimit);var p=i.getValueOrDefault(d.fontColor,u.defaultFontColor),v=n(d),b=c.drawTicks?c.tickMarkLength:0,y=i.getValueOrDefault(c.borderDash,u.borderDash),x=i.getValueOrDefault(c.borderDashOffset,u.borderDashOffset),k=i.getValueOrDefault(h.fontColor,u.defaultFontColor),_=n(h),w=i.toRadians(a.labelRotation),S=Math.cos(w),M=a.longestLabelWidth*S;l.fillStyle=p;var D=[];if(m){if(o=!1,f&&(M/=2),(M+d.autoSkipPadding)*a.ticks.length>a.width-(a.paddingLeft+a.paddingRight)&&(o=1+Math.floor((M+d.autoSkipPadding)*a.ticks.length/(a.width-(a.paddingLeft+a.paddingRight)))),s&&a.ticks.length>s)for(;!o||a.ticks.length/(o||1)>s;)o||(o=1),o+=1;g||(o=!1)}var C="right"===r.position?a.left:a.right-b,T="right"===r.position?a.left+b:a.right,P="bottom"===r.position?a.top:a.bottom-b,I="bottom"===r.position?a.top+b:a.bottom;if(i.each(a.ticks,function(t,n){if(void 0!==t&&null!==t){var s=a.ticks.length===n+1,l=o>1&&n%o>0||n%o===0&&n+o>=a.ticks.length;if((!l||s)&&void 0!==t&&null!==t){var u,h;n===("undefined"!=typeof a.zeroLineIndex?a.zeroLineIndex:0)?(u=c.zeroLineWidth,h=c.zeroLineColor):(u=i.getValueAtIndexOrDefault(c.lineWidth,n),h=i.getValueAtIndexOrDefault(c.color,n));var g,p,v,k,_,S,M,A,F,O,R="middle",L="middle";if(m){"bottom"===r.position?(L=f?"middle":"top",R=f?"right":"center",O=a.top+b):(L=f?"middle":"bottom",R=f?"left":"center",O=a.bottom-b);var V=a.getPixelForTick(n)+i.aliasPixel(u);F=a.getPixelForTick(n,c.offsetGridLines)+d.labelOffset,g=v=_=M=V,p=P,k=I,S=e.top,A=e.bottom}else{var W,Y="left"===r.position,B=d.padding;d.mirror?(R=Y?"left":"right",W=B):(R=Y?"right":"left",W=b+B),F=Y?a.right-W:a.left+W;var z=a.getPixelForTick(n);z+=i.aliasPixel(u),O=a.getPixelForTick(n,c.offsetGridLines),g=C,v=T,_=e.left,M=e.right,p=k=S=A=z}D.push({tx1:g,ty1:p,tx2:v,ty2:k,x1:_,y1:S,x2:M,y2:A,labelX:F,labelY:O,glWidth:u,glColor:h,glBorderDash:y,glBorderDashOffset:x,rotation:-1*w,label:t,textBaseline:L,textAlign:R})}}}),i.each(D,function(t){if(c.display&&(l.save(),l.lineWidth=t.glWidth,l.strokeStyle=t.glColor,l.setLineDash&&(l.setLineDash(t.glBorderDash),l.lineDashOffset=t.glBorderDashOffset),l.beginPath(),c.drawTicks&&(l.moveTo(t.tx1,t.ty1),l.lineTo(t.tx2,t.ty2)),c.drawOnChartArea&&(l.moveTo(t.x1,t.y1),l.lineTo(t.x2,t.y2)),l.stroke(),l.restore()),d.display){l.save(),l.translate(t.labelX,t.labelY),l.rotate(t.rotation),l.font=v.font,l.textBaseline=t.textBaseline,l.textAlign=t.textAlign;var e=t.label;if(i.isArray(e))for(var n=0,a=0;n0)i=t.stepSize;else{var r=e.niceNum(n.max-n.min,!1);i=e.niceNum(r/(t.maxTicks-1),!0)}var o=Math.floor(n.min/i)*i,s=Math.ceil(n.max/i)*i;t.min&&t.max&&t.stepSize&&e.almostWhole((t.max-t.min)/t.stepSize,i/1e3)&&(o=t.min,s=t.max);var l=(s-o)/i;l=e.almostEquals(l,Math.round(l),i/1e3)?Math.round(l):Math.ceil(l),a.push(void 0!==t.min?t.min:o);for(var u=1;u3?i[2]-i[1]:i[1]-i[0];Math.abs(a)>1&&t!==Math.floor(t)&&(a=t-Math.floor(t));var r=e.log10(Math.abs(a)),o="";if(0!==t){var s=-1*Math.floor(r);s=Math.max(Math.min(s,20),0),o=t.toFixed(s)}else o="0";return o},logarithmic:function(t,n,i){var a=t/Math.pow(10,Math.floor(e.log10(t)));return 0===t?"0":1===a||2===a||5===a||0===n||n===i.length-1?t.toExponential():""}}}}},{}],35:[function(t,e,n){"use strict";e.exports=function(t){function e(e,n){var i=new t.Title({ctx:e.chart.ctx,options:n,chart:e});e.titleBlock=i,t.layoutService.addBox(e,i)}var n=t.helpers;t.defaults.global.title={display:!1,position:"top",fullWidth:!0,fontStyle:"bold",padding:10,text:""};var i=n.noop;t.Title=t.Element.extend({initialize:function(t){var e=this;n.extend(e,t),e.legendHitBoxes=[]},beforeUpdate:i,update:function(t,e,n){var i=this;return i.beforeUpdate(),i.maxWidth=t,i.maxHeight=e,i.margins=n,i.beforeSetDimensions(),i.setDimensions(),i.afterSetDimensions(),i.beforeBuildLabels(),i.buildLabels(),i.afterBuildLabels(),i.beforeFit(),i.fit(),i.afterFit(),i.afterUpdate(),i.minSize},afterUpdate:i,beforeSetDimensions:i,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:i,beforeBuildLabels:i,buildLabels:i,afterBuildLabels:i,beforeFit:i,fit:function(){var e=this,i=n.getValueOrDefault,a=e.options,r=t.defaults.global,o=a.display,s=i(a.fontSize,r.defaultFontSize),l=e.minSize;e.isHorizontal()?(l.width=e.maxWidth,l.height=o?s+2*a.padding:0):(l.width=o?s+2*a.padding:0,l.height=e.maxHeight),e.width=l.width,e.height=l.height},afterFit:i,isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},draw:function(){var e=this,i=e.ctx,a=n.getValueOrDefault,r=e.options,o=t.defaults.global;if(r.display){var s,l,u,d=a(r.fontSize,o.defaultFontSize),c=a(r.fontStyle,o.defaultFontStyle),h=a(r.fontFamily,o.defaultFontFamily),f=n.fontString(d,c,h),g=0,m=e.top,p=e.left,v=e.bottom,b=e.right;i.fillStyle=a(r.fontColor,o.defaultFontColor),i.font=f,e.isHorizontal()?(s=p+(b-p)/2,l=m+(v-m)/2,u=b-p):(s="left"===r.position?p+d/2:b-d/2,l=m+(v-m)/2,u=v-m,g=Math.PI*("left"===r.position?-.5:.5)),i.save(),i.translate(s,l),i.rotate(g),i.textAlign="center",i.textBaseline="middle",i.fillText(r.text,0,0,u),i.restore()}}}),t.plugins.register({beforeInit:function(t){var n=t.options.title;n&&e(t,n)},beforeUpdate:function(i){var a=i.options.title;a?(a=n.configMerge(t.defaults.global.title,a),i.titleBlock?i.titleBlock.options=a:e(i,a)):(t.layoutService.removeBox(i,i.titleBlock),delete i.titleBlock)}})}},{}],36:[function(t,e,n){"use strict";e.exports=function(t){function e(t,e){var n=l.color(t);return n.alpha(e*n.alpha()).rgbaString()}function n(t,e){return e&&(l.isArray(e)?Array.prototype.push.apply(t,e):t.push(e)),t}function i(t){var e=t._xScale,n=t._yScale||t._scale,i=t._index,a=t._datasetIndex;return{xLabel:e?e.getLabelForIndex(i,a):"",yLabel:n?n.getLabelForIndex(i,a):"",index:i,datasetIndex:a,x:t._model.x,y:t._model.y}}function a(e){var n=t.defaults.global,i=l.getValueOrDefault;return{xPadding:e.xPadding,yPadding:e.yPadding,xAlign:e.xAlign,yAlign:e.yAlign,bodyFontColor:e.bodyFontColor,_bodyFontFamily:i(e.bodyFontFamily,n.defaultFontFamily),_bodyFontStyle:i(e.bodyFontStyle,n.defaultFontStyle),_bodyAlign:e.bodyAlign,bodyFontSize:i(e.bodyFontSize,n.defaultFontSize),bodySpacing:e.bodySpacing,titleFontColor:e.titleFontColor,_titleFontFamily:i(e.titleFontFamily,n.defaultFontFamily),_titleFontStyle:i(e.titleFontStyle,n.defaultFontStyle),titleFontSize:i(e.titleFontSize,n.defaultFontSize),_titleAlign:e.titleAlign,titleSpacing:e.titleSpacing, -titleMarginBottom:e.titleMarginBottom,footerFontColor:e.footerFontColor,_footerFontFamily:i(e.footerFontFamily,n.defaultFontFamily),_footerFontStyle:i(e.footerFontStyle,n.defaultFontStyle),footerFontSize:i(e.footerFontSize,n.defaultFontSize),_footerAlign:e.footerAlign,footerSpacing:e.footerSpacing,footerMarginTop:e.footerMarginTop,caretSize:e.caretSize,cornerRadius:e.cornerRadius,backgroundColor:e.backgroundColor,opacity:0,legendColorBackground:e.multiKeyBackground,displayColors:e.displayColors}}function r(t,e){var n=t._chart.ctx,i=2*e.yPadding,a=0,r=e.body,o=r.reduce(function(t,e){return t+e.before.length+e.lines.length+e.after.length},0);o+=e.beforeBody.length+e.afterBody.length;var s=e.title.length,u=e.footer.length,d=e.titleFontSize,c=e.bodyFontSize,h=e.footerFontSize;i+=s*d,i+=s?(s-1)*e.titleSpacing:0,i+=s?e.titleMarginBottom:0,i+=o*c,i+=o?(o-1)*e.bodySpacing:0,i+=u?e.footerMarginTop:0,i+=u*h,i+=u?(u-1)*e.footerSpacing:0;var f=0,g=function(t){a=Math.max(a,n.measureText(t).width+f)};return n.font=l.fontString(d,e._titleFontStyle,e._titleFontFamily),l.each(e.title,g),n.font=l.fontString(c,e._bodyFontStyle,e._bodyFontFamily),l.each(e.beforeBody.concat(e.afterBody),g),f=e.displayColors?c+2:0,l.each(r,function(t){l.each(t.before,g),l.each(t.lines,g),l.each(t.after,g)}),f=0,n.font=l.fontString(h,e._footerFontStyle,e._footerFontFamily),l.each(e.footer,g),a+=2*e.xPadding,{width:a,height:i}}function o(t,e){var n=t._model,i=t._chart,a=t._chartInstance.chartArea,r="center",o="center";n.yi.height-e.height&&(o="bottom");var s,l,u,d,c,h=(a.left+a.right)/2,f=(a.top+a.bottom)/2;"center"===o?(s=function(t){return t<=h},l=function(t){return t>h}):(s=function(t){return t<=e.width/2},l=function(t){return t>=i.width-e.width/2}),u=function(t){return t+e.width>i.width},d=function(t){return t-e.width<0},c=function(t){return t<=f?"top":"bottom"},s(n.x)?(r="left",u(n.x)&&(r="center",o=c(n.y))):l(n.x)&&(r="right",d(n.x)&&(r="center",o=c(n.y)));var g=t._options;return{xAlign:g.xAlign?g.xAlign:r,yAlign:g.yAlign?g.yAlign:o}}function s(t,e,n){var i=t.x,a=t.y,r=t.caretSize,o=t.caretPadding,s=t.cornerRadius,l=n.xAlign,u=n.yAlign,d=r+o,c=s+o;return"right"===l?i-=e.width:"center"===l&&(i-=e.width/2),"top"===u?a+=d:a-="bottom"===u?e.height+d:e.height/2,"center"===u?"left"===l?i+=d:"right"===l&&(i-=d):"left"===l?i-=c:"right"===l&&(i+=c),{x:i,y:a}}var l=t.helpers;t.defaults.global.tooltips={enabled:!0,custom:null,mode:"nearest",position:"average",intersect:!0,backgroundColor:"rgba(0,0,0,0.8)",titleFontStyle:"bold",titleSpacing:2,titleMarginBottom:6,titleFontColor:"#fff",titleAlign:"left",bodySpacing:2,bodyFontColor:"#fff",bodyAlign:"left",footerFontStyle:"bold",footerSpacing:2,footerMarginTop:6,footerFontColor:"#fff",footerAlign:"left",yPadding:6,xPadding:6,caretSize:5,cornerRadius:6,multiKeyBackground:"#fff",displayColors:!0,callbacks:{beforeTitle:l.noop,title:function(t,e){var n="",i=e.labels,a=i?i.length:0;if(t.length>0){var r=t[0];r.xLabel?n=r.xLabel:a>0&&r.indexl;)r-=2*Math.PI;for(;r=s&&r<=l,d=o>=i.innerRadius&&o<=i.outerRadius;return u&&d}return!1},getCenterPoint:function(){var t=this._view,e=(t.startAngle+t.endAngle)/2,n=(t.innerRadius+t.outerRadius)/2;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},getArea:function(){var t=this._view;return Math.PI*((t.endAngle-t.startAngle)/(2*Math.PI))*(Math.pow(t.outerRadius,2)-Math.pow(t.innerRadius,2))},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,n=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},draw:function(){var t=this._chart.ctx,e=this._view,n=e.startAngle,i=e.endAngle;t.beginPath(),t.arc(e.x,e.y,e.outerRadius,n,i),t.arc(e.x,e.y,e.innerRadius,i,n,!0),t.closePath(),t.strokeStyle=e.borderColor,t.lineWidth=e.borderWidth,t.fillStyle=e.backgroundColor,t.fill(),t.lineJoin="bevel",e.borderWidth&&t.stroke()}})}},{}],38:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=t.defaults.global;t.defaults.global.elements.line={tension:.4,backgroundColor:n.defaultColor,borderWidth:3,borderColor:n.defaultColor,borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",capBezierPoints:!0,fill:!0},t.elements.Line=t.Element.extend({draw:function(){function t(t,e){var n=e._view;e._view.steppedLine===!0?(l.lineTo(n.x,t._view.y),l.lineTo(n.x,n.y)):0===e._view.tension?l.lineTo(n.x,n.y):l.bezierCurveTo(t._view.controlPointNextX,t._view.controlPointNextY,n.controlPointPreviousX,n.controlPointPreviousY,n.x,n.y)}var i=this,a=i._view,r=a.spanGaps,o=a.scaleZero,s=i._loop;s||("top"===a.fill?o=a.scaleTop:"bottom"===a.fill&&(o=a.scaleBottom));var l=i._chart.ctx;l.save();var u=i._children.slice(),d=-1;s&&u.length&&u.push(u[0]);var c,h,f,g;if(u.length&&a.fill){for(l.beginPath(),c=0;ce?1:-1,o=1,s=u.borderSkipped||"left"):(e=u.x-u.width/2,n=u.x+u.width/2,i=u.y,a=u.base,r=1,o=a>i?1:-1,s=u.borderSkipped||"bottom"),d){var c=Math.min(Math.abs(e-n),Math.abs(i-a));d=d>c?c:d;var h=d/2,f=e+("left"!==s?h*r:0),g=n+("right"!==s?-h*r:0),m=i+("top"!==s?h*o:0),p=a+("bottom"!==s?-h*o:0);f!==g&&(i=m,a=p),m!==p&&(e=f,n=g)}l.beginPath(),l.fillStyle=u.backgroundColor,l.strokeStyle=u.borderColor,l.lineWidth=d;var v=[[e,a],[e,i],[n,i],[n,a]],b=["bottom","left","top","right"],y=b.indexOf(s,0);y===-1&&(y=0);var x=t(0);l.moveTo(x[0],x[1]);for(var k=1;k<4;k++)x=t(k),l.lineTo(x[0],x[1]);l.fill(),d&&l.stroke()},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){var i=!1;if(this._view){var a=n(this);i=t>=a.left&&t<=a.right&&e>=a.top&&e<=a.bottom}return i},inLabelRange:function(t,i){var a=this;if(!a._view)return!1;var r=!1,o=n(a);return r=e(a)?t>=o.left&&t<=o.right:i>=o.top&&i<=o.bottom},inXRange:function(t){var e=n(this);return t>=e.left&&t<=e.right},inYRange:function(t){var e=n(this);return t>=e.top&&t<=e.bottom},getCenterPoint:function(){var t,n,i=this._view;return e(this)?(t=i.x,n=(i.y+i.base)/2):(t=(i.x+i.base)/2,n=i.y),{x:t,y:n}},getArea:function(){var t=this._view;return t.width*Math.abs(t.y-t.base)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}})}},{}],41:[function(t,e,n){"use strict";e.exports=function(t){function e(t,e){var n=l.getStyle(t,e),i=n&&n.match(/(\d+)px/);return i?Number(i[1]):void 0}function n(t,n){var i=t.style,a=t.getAttribute("height"),r=t.getAttribute("width");if(t._chartjs={initial:{height:a,width:r,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",null===r||""===r){var o=e(t,"width");void 0!==o&&(t.width=o)}if(null===a||""===a)if(""===t.style.height)t.height=t.width/(n.options.aspectRatio||2);else{var s=e(t,"height");void 0!==o&&(t.height=s)}return t}function i(t,e,n,i,a){return{type:t,chart:e,native:a||null,x:void 0!==n?n:null,y:void 0!==i?i:null}}function a(t,e){var n=u[t.type]||t.type,a=l.getRelativePosition(t,e);return i(n,e,a.x,a.y,t)}function r(t){var e=document.createElement("iframe");return e.className="chartjs-hidden-iframe",e.style.cssText="display:block;overflow:hidden;border:0;margin:0;top:0;left:0;bottom:0;right:0;height:100%;width:100%;position:absolute;pointer-events:none;z-index:-1;",e.tabIndex=-1,l.addEvent(e,"load",function(){l.addEvent(e.contentWindow||e,"resize",t),t()}),e}function o(t,e,n){var a=t._chartjs={ticking:!1},o=function(){a.ticking||(a.ticking=!0,l.requestAnimFrame.call(window,function(){if(a.resizer)return a.ticking=!1,e(i("resize",n))}))};a.resizer=r(o),t.insertBefore(a.resizer,t.firstChild)}function s(t){if(t&&t._chartjs){var e=t._chartjs.resizer;e&&(e.parentNode.removeChild(e),t._chartjs.resizer=null),delete t._chartjs}}var l=t.helpers,u={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};return{acquireContext:function(t,e){if("string"==typeof t?t=document.getElementById(t):t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t instanceof HTMLCanvasElement){var i=t.getContext&&t.getContext("2d");if(i instanceof CanvasRenderingContext2D)return n(t,e),i}return null},releaseContext:function(t){var e=t.canvas;if(e._chartjs){var n=e._chartjs.initial;["height","width"].forEach(function(t){var i=n[t];void 0===i||null===i?e.removeAttribute(t):e.setAttribute(t,i)}),l.each(n.style||{},function(t,n){e.style[n]=t}),e.width=e.width,delete e._chartjs}},addEventListener:function(t,e,n){var i=t.chart.canvas;if("resize"===e)return void o(i.parentNode,n,t.chart);var r=n._chartjs||(n._chartjs={}),s=r.proxies||(r.proxies={}),u=s[t.id+"_"+e]=function(e){n(a(e,t.chart))};l.addEvent(i,e,u)},removeEventListener:function(t,e,n){var i=t.chart.canvas;if("resize"===e)return void s(i.parentNode,n);var a=n._chartjs||{},r=a.proxies||{},o=r[t.id+"_"+e];o&&l.removeEvent(i,e,o)}}}},{}],42:[function(t,e,n){"use strict";var i=t(41);e.exports=function(t){t.platform={acquireContext:function(){},releaseContext:function(){},addEventListener:function(){},removeEventListener:function(){}},t.helpers.extend(t.platform,i(t))}},{41:41}],43:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n={position:"bottom"},i=t.Scale.extend({getLabels:function(){var t=this.chart.data;return(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels},determineDataLimits:function(){var t=this,n=t.getLabels();t.minIndex=0,t.maxIndex=n.length-1;var i;void 0!==t.options.ticks.min&&(i=e.indexOf(n,t.options.ticks.min),t.minIndex=i!==-1?i:t.minIndex),void 0!==t.options.ticks.max&&(i=e.indexOf(n,t.options.ticks.max),t.maxIndex=i!==-1?i:t.maxIndex),t.min=n[t.minIndex],t.max=n[t.maxIndex]},buildTicks:function(){var t=this,e=t.getLabels();t.ticks=0===t.minIndex&&t.maxIndex===e.length-1?e:e.slice(t.minIndex,t.maxIndex+1)},getLabelForIndex:function(t,e){var n=this,i=n.chart.data,a=n.isHorizontal();return i.yLabels&&!a?n.getRightValue(i.datasets[e].data[t]):n.ticks[t-n.minIndex]},getPixelForValue:function(t,e,n,i){var a=this,r=Math.max(a.maxIndex+1-a.minIndex-(a.options.gridLines.offsetGridLines?0:1),1);if(void 0!==t&&isNaN(e)){var o=a.getLabels(),s=o.indexOf(t);e=s!==-1?s:e}if(a.isHorizontal()){var l=a.width/r,u=l*(e-a.minIndex);return(a.options.gridLines.offsetGridLines&&i||a.maxIndex===a.minIndex&&i)&&(u+=l/2),a.left+Math.round(u)}var d=a.height/r,c=d*(e-a.minIndex);return a.options.gridLines.offsetGridLines&&i&&(c+=d/2),a.top+Math.round(c)},getPixelForTick:function(t,e){return this.getPixelForValue(this.ticks[t],t+this.minIndex,null,e)},getValueForPixel:function(t){var e,n=this,i=Math.max(n.ticks.length-(n.options.gridLines.offsetGridLines?0:1),1),a=n.isHorizontal(),r=(a?n.width:n.height)/i;return t-=a?n.left:n.top,n.options.gridLines.offsetGridLines&&(t-=r/2),e=t<=0?0:Math.round(t/r)},getBasePixel:function(){return this.bottom}});t.scaleService.registerScaleType("category",i,n)}},{}],44:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n={position:"left",ticks:{callback:t.Ticks.formatters.linear}},i=t.LinearScaleBase.extend({determineDataLimits:function(){function t(t){return s?t.xAxisID===n.id:t.yAxisID===n.id}var n=this,i=n.options,a=n.chart,r=a.data,o=r.datasets,s=n.isHorizontal();n.min=null,n.max=null;var l=i.stacked;if(void 0===l&&e.each(o,function(e,n){if(!l){var i=a.getDatasetMeta(n);a.isDatasetVisible(n)&&t(i)&&void 0!==i.stack&&(l=!0)}}),i.stacked||l){var u={};e.each(o,function(r,o){var s=a.getDatasetMeta(o),l=[s.type,void 0===i.stacked&&void 0===s.stack?o:"",s.stack].join(".");void 0===u[l]&&(u[l]={positiveValues:[],negativeValues:[]});var d=u[l].positiveValues,c=u[l].negativeValues;a.isDatasetVisible(o)&&t(s)&&e.each(r.data,function(t,e){var a=+n.getRightValue(t);isNaN(a)||s.data[e].hidden||(d[e]=d[e]||0,c[e]=c[e]||0,i.relativePoints?d[e]=100:a<0?c[e]+=a:d[e]+=a)})}),e.each(u,function(t){var i=t.positiveValues.concat(t.negativeValues),a=e.min(i),r=e.max(i);n.min=null===n.min?a:Math.min(n.min,a),n.max=null===n.max?r:Math.max(n.max,r)})}else e.each(o,function(i,r){var o=a.getDatasetMeta(r);a.isDatasetVisible(r)&&t(o)&&e.each(i.data,function(t,e){var i=+n.getRightValue(t);isNaN(i)||o.data[e].hidden||(null===n.min?n.min=i:in.max&&(n.max=i))})});this.handleTickRangeOptions()},getTickLimit:function(){var n,i=this,a=i.options.ticks;if(i.isHorizontal())n=Math.min(a.maxTicksLimit?a.maxTicksLimit:11,Math.ceil(i.width/50));else{var r=e.getValueOrDefault(a.fontSize,t.defaults.global.defaultFontSize);n=Math.min(a.maxTicksLimit?a.maxTicksLimit:11,Math.ceil(i.height/(2*r)))}return n},handleDirectionalChanges:function(){this.isHorizontal()||this.ticks.reverse()},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},getPixelForValue:function(t){var e,n=this,i=n.start,a=+n.getRightValue(t),r=n.end-i;return n.isHorizontal()?(e=n.left+n.width/r*(a-i),Math.round(e)):(e=n.bottom-n.height/r*(a-i),Math.round(e))},getValueForPixel:function(t){var e=this,n=e.isHorizontal(),i=n?e.width:e.height,a=(n?t-e.left:e.bottom-t)/i;return e.start+(e.end-e.start)*a},getPixelForTick:function(t){return this.getPixelForValue(this.ticksAsNumbers[t])}});t.scaleService.registerScaleType("linear",i,n)}},{}],45:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=e.noop;t.LinearScaleBase=t.Scale.extend({handleTickRangeOptions:function(){var t=this,n=t.options,i=n.ticks;if(i.beginAtZero){var a=e.sign(t.min),r=e.sign(t.max);a<0&&r<0?t.max=0:a>0&&r>0&&(t.min=0)}void 0!==i.min?t.min=i.min:void 0!==i.suggestedMin&&(t.min=Math.min(t.min,i.suggestedMin)),void 0!==i.max?t.max=i.max:void 0!==i.suggestedMax&&(t.max=Math.max(t.max,i.suggestedMax)),t.min===t.max&&(t.max++,i.beginAtZero||t.min--)},getTickLimit:n,handleDirectionalChanges:n,buildTicks:function(){var n=this,i=n.options,a=i.ticks,r=n.getTickLimit();r=Math.max(2,r);var o={maxTicks:r,min:a.min,max:a.max,stepSize:e.getValueOrDefault(a.fixedStepSize,a.stepSize)},s=n.ticks=t.Ticks.generators.linear(o,n);n.handleDirectionalChanges(),n.max=e.max(s),n.min=e.min(s),a.reverse?(s.reverse(),n.start=n.max,n.end=n.min):(n.start=n.min,n.end=n.max)},convertTicksToLabels:function(){var e=this;e.ticksAsNumbers=e.ticks.slice(),e.zeroLineIndex=e.ticks.indexOf(0),t.Scale.prototype.convertTicksToLabels.call(e)}})}},{}],46:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n={position:"left",ticks:{callback:t.Ticks.formatters.logarithmic}},i=t.Scale.extend({determineDataLimits:function(){function t(t){return u?t.xAxisID===n.id:t.yAxisID===n.id}var n=this,i=n.options,a=i.ticks,r=n.chart,o=r.data,s=o.datasets,l=e.getValueOrDefault,u=n.isHorizontal();n.min=null,n.max=null,n.minNotZero=null;var d=i.stacked;if(void 0===d&&e.each(s,function(e,n){if(!d){var i=r.getDatasetMeta(n);r.isDatasetVisible(n)&&t(i)&&void 0!==i.stack&&(d=!0)}}),i.stacked||d){var c={};e.each(s,function(a,o){var s=r.getDatasetMeta(o),l=[s.type,void 0===i.stacked&&void 0===s.stack?o:"",s.stack].join(".");r.isDatasetVisible(o)&&t(s)&&(void 0===c[l]&&(c[l]=[]),e.each(a.data,function(t,e){var a=c[l],r=+n.getRightValue(t);isNaN(r)||s.data[e].hidden||(a[e]=a[e]||0,i.relativePoints?a[e]=100:a[e]+=r)}))}),e.each(c,function(t){var i=e.min(t),a=e.max(t);n.min=null===n.min?i:Math.min(n.min,i),n.max=null===n.max?a:Math.max(n.max,a)})}else e.each(s,function(i,a){var o=r.getDatasetMeta(a);r.isDatasetVisible(a)&&t(o)&&e.each(i.data,function(t,e){var i=+n.getRightValue(t);isNaN(i)||o.data[e].hidden||(null===n.min?n.min=i:in.max&&(n.max=i),0!==i&&(null===n.minNotZero||ia?{start:e-n-5,end:e}:{start:e,end:e+n+5}}function r(t){var r,o,s,l=n(t),u=Math.min(t.height/2,t.width/2),d={l:t.width,r:0,t:t.height,b:0},c={};t.ctx.font=l.font,t._pointLabelSizes=[];var h=e(t);for(r=0;rd.r&&(d.r=p.end,c.r=g),v.startd.b&&(d.b=v.end,c.b=g)}t.setReductions(u,d,c)}function o(t){var e=Math.min(t.height/2,t.width/2);t.drawingArea=Math.round(e),t.setCenterPoint(0,0,0,0)}function s(t){return 0===t||180===t?"center":t<180?"left":"right"}function l(t,e,n,i){if(f.isArray(e))for(var a=n.y,r=1.5*i,o=0;o270||t<90)&&(n.y-=e.h)}function d(t){var i=t.ctx,a=f.getValueOrDefault,r=t.options,o=r.angleLines,d=r.pointLabels;i.lineWidth=o.lineWidth,i.strokeStyle=o.color;var c=t.getDistanceFromCenterForValue(r.reverse?t.min:t.max),h=n(t);i.textBaseline="top";for(var m=e(t)-1;m>=0;m--){if(o.display){var p=t.getPointPosition(m,c);i.beginPath(),i.moveTo(t.xCenter,t.yCenter),i.lineTo(p.x,p.y),i.stroke(),i.closePath()}var v=t.getPointPosition(m,c+5),b=a(d.fontColor,g.defaultFontColor);i.font=h.font,i.fillStyle=b;var y=t.getIndexAngle(m),x=f.toDegrees(y);i.textAlign=s(x),u(x,t._pointLabelSizes[m],v),l(i,t.pointLabels[m]||"",v,h.size)}}function c(t,n,i,a){var r=t.ctx;if(r.strokeStyle=f.getValueAtIndexOrDefault(n.color,a-1),r.lineWidth=f.getValueAtIndexOrDefault(n.lineWidth,a-1),t.options.lineArc)r.beginPath(),r.arc(t.xCenter,t.yCenter,i,0,2*Math.PI),r.closePath(),r.stroke();else{var o=e(t);if(0===o)return;r.beginPath();var s=t.getPointPosition(0,i);r.moveTo(s.x,s.y);for(var l=1;l0&&n>0?e:0)},draw:function(){var t=this,e=t.options,n=e.gridLines,i=e.ticks,a=f.getValueOrDefault; -if(e.display){var r=t.ctx,o=a(i.fontSize,g.defaultFontSize),s=a(i.fontStyle,g.defaultFontStyle),l=a(i.fontFamily,g.defaultFontFamily),u=f.fontString(o,s,l);f.each(t.ticks,function(s,l){if(l>0||e.reverse){var d=t.getDistanceFromCenterForValue(t.ticksAsNumbers[l]),h=t.yCenter-d;if(n.display&&0!==l&&c(t,n,d,l),i.display){var f=a(i.fontColor,g.defaultFontColor);if(r.font=u,i.showLabelBackdrop){var m=r.measureText(s).width;r.fillStyle=i.backdropColor,r.fillRect(t.xCenter-m/2-i.backdropPaddingX,h-o/2-i.backdropPaddingY,m+2*i.backdropPaddingX,o+2*i.backdropPaddingY)}r.textAlign="center",r.textBaseline="middle",r.fillStyle=f,r.fillText(s,t.xCenter,h)}}}),e.lineArc||d(t)}}});t.scaleService.registerScaleType("radialLinear",p,m)}},{}],48:[function(t,e,n){"use strict";var i=t(6);i="function"==typeof i?i:window.moment,e.exports=function(t){var e=t.helpers,n={units:[{name:"millisecond",steps:[1,2,5,10,20,50,100,250,500]},{name:"second",steps:[1,2,5,10,30]},{name:"minute",steps:[1,2,5,10,30]},{name:"hour",steps:[1,2,3,6,12]},{name:"day",steps:[1,2,5]},{name:"week",maxStep:4},{name:"month",maxStep:3},{name:"quarter",maxStep:4},{name:"year",maxStep:!1}]},a={position:"bottom",time:{parser:!1,format:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm:ss a",hour:"MMM D, hA",day:"ll",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"}},ticks:{autoSkip:!1}},r=t.Scale.extend({initialize:function(){if(!i)throw new Error("Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com");t.Scale.prototype.initialize.call(this)},getLabelMoment:function(t,e){return null===t||null===e?null:"undefined"!=typeof this.labelMoments[t]?this.labelMoments[t][e]:null},getLabelDiff:function(t,e){var n=this;return null===t||null===e?null:(void 0===n.labelDiffs&&n.buildLabelDiffs(),"undefined"!=typeof n.labelDiffs[t]?n.labelDiffs[t][e]:null)},getMomentStartOf:function(t){var e=this;return"week"===e.options.time.unit&&e.options.time.isoWeekday!==!1?t.clone().startOf("isoWeek").isoWeekday(e.options.time.isoWeekday):t.clone().startOf(e.tickUnit)},determineDataLimits:function(){var t=this;t.labelMoments=[];var n=[];t.chart.data.labels&&t.chart.data.labels.length>0?(e.each(t.chart.data.labels,function(e){var i=t.parseTime(e);i.isValid()&&(t.options.time.round&&i.startOf(t.options.time.round),n.push(i))},t),t.firstTick=i.min.call(t,n),t.lastTick=i.max.call(t,n)):(t.firstTick=null,t.lastTick=null),e.each(t.chart.data.datasets,function(a,r){var o=[],s=t.chart.isDatasetVisible(r);"object"==typeof a.data[0]&&null!==a.data[0]?e.each(a.data,function(e){var n=t.parseTime(t.getRightValue(e));n.isValid()&&(t.options.time.round&&n.startOf(t.options.time.round),o.push(n),s&&(t.firstTick=null!==t.firstTick?i.min(t.firstTick,n):n,t.lastTick=null!==t.lastTick?i.max(t.lastTick,n):n))},t):o=n,t.labelMoments.push(o)},t),t.options.time.min&&(t.firstTick=t.parseTime(t.options.time.min)),t.options.time.max&&(t.lastTick=t.parseTime(t.options.time.max)),t.firstTick=(t.firstTick||i()).clone(),t.lastTick=(t.lastTick||i()).clone()},buildLabelDiffs:function(){var t=this;t.labelDiffs=[];var n=[];t.chart.data.labels&&t.chart.data.labels.length>0&&e.each(t.chart.data.labels,function(e){var i=t.parseTime(e);i.isValid()&&(t.options.time.round&&i.startOf(t.options.time.round),n.push(i.diff(t.firstTick,t.tickUnit,!0)))},t),e.each(t.chart.data.datasets,function(i){var a=[];"object"==typeof i.data[0]&&null!==i.data[0]?e.each(i.data,function(e){var n=t.parseTime(t.getRightValue(e));n.isValid()&&(t.options.time.round&&n.startOf(t.options.time.round),a.push(n.diff(t.firstTick,t.tickUnit,!0)))},t):a=n,t.labelDiffs.push(a)},t)},buildTicks:function(){var i=this;i.ctx.save();var a=e.getValueOrDefault(i.options.ticks.fontSize,t.defaults.global.defaultFontSize),r=e.getValueOrDefault(i.options.ticks.fontStyle,t.defaults.global.defaultFontStyle),o=e.getValueOrDefault(i.options.ticks.fontFamily,t.defaults.global.defaultFontFamily),s=e.fontString(a,r,o);if(i.ctx.font=s,i.ticks=[],i.unitScale=1,i.scaleSizeInUnits=0,i.options.time.unit)i.tickUnit=i.options.time.unit||"day",i.displayFormat=i.options.time.displayFormats[i.tickUnit],i.scaleSizeInUnits=i.lastTick.diff(i.firstTick,i.tickUnit,!0),i.unitScale=e.getValueOrDefault(i.options.time.unitStepSize,1);else{var l=i.isHorizontal()?i.width:i.height,u=i.tickFormatFunction(i.firstTick,0,[]),d=i.ctx.measureText(u).width,c=Math.cos(e.toRadians(i.options.ticks.maxRotation)),h=Math.sin(e.toRadians(i.options.ticks.maxRotation));d=d*c+a*h;var f=l/d;i.tickUnit=i.options.time.minUnit,i.scaleSizeInUnits=i.lastTick.diff(i.firstTick,i.tickUnit,!0),i.displayFormat=i.options.time.displayFormats[i.tickUnit];for(var g=0,m=n.units[g];g=Math.ceil(i.scaleSizeInUnits/f)){i.unitScale=e.getValueOrDefault(i.options.time.unitStepSize,m.steps[p]);break}break}if(m.maxStep===!1||Math.ceil(i.scaleSizeInUnits/f)=0&&(i.lastTick=x),i.scaleSizeInUnits=i.lastTick.diff(i.firstTick,i.tickUnit,!0)}i.options.time.displayFormat&&(i.displayFormat=i.options.time.displayFormat),i.ticks.push(i.firstTick.clone());for(var _=i.unitScale;_<=i.scaleSizeInUnits;_+=i.unitScale){var w=y.clone().add(_,i.tickUnit);if(i.options.time.max&&w.diff(i.lastTick,i.tickUnit,!0)>=0)break;i.ticks.push(w)}var S=i.ticks[i.ticks.length-1].diff(i.lastTick,i.tickUnit);0===S&&0!==i.scaleSizeInUnits||(i.options.time.max?(i.ticks.push(i.lastTick.clone()),i.scaleSizeInUnits=i.lastTick.diff(i.ticks[0],i.tickUnit,!0)):(i.ticks.push(i.lastTick.clone()),i.scaleSizeInUnits=i.lastTick.diff(i.firstTick,i.tickUnit,!0))),i.ctx.restore(),i.labelDiffs=void 0},getLabelForIndex:function(t,e){var n=this,i=n.chart.data.labels&&tn?(e+.05)/(n+.05):(n+.05)/(e+.05)},level:function(t){var e=this.contrast(t);return e>=7.1?"AAA":e>=4.5?"AA":""},dark:function(){var t=this.values.rgb,e=(299*t[0]+587*t[1]+114*t[2])/1e3;return e<128},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;e<3;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){var e=this.values.hsl;return e[2]+=e[2]*t,this.setValues("hsl",e),this},darken:function(t){var e=this.values.hsl;return e[2]-=e[2]*t,this.setValues("hsl",e),this},saturate:function(t){var e=this.values.hsl;return e[1]+=e[1]*t,this.setValues("hsl",e),this},desaturate:function(t){var e=this.values.hsl;return e[1]-=e[1]*t,this.setValues("hsl",e),this},whiten:function(t){var e=this.values.hwb;return e[1]+=e[1]*t,this.setValues("hwb",e),this},blacken:function(t){var e=this.values.hwb;return e[2]+=e[2]*t,this.setValues("hwb",e),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){var e=this.values.alpha;return this.setValues("alpha",e-e*t),this},opaquer:function(t){var e=this.values.alpha;return this.setValues("alpha",e+e*t),this},rotate:function(t){var e=this.values.hsl,n=(e[0]+t)%360;return e[0]=n<0?360+n:n,this.setValues("hsl",e),this},mix:function(t,e){var n=this,i=t,a=void 0===e?.5:e,r=2*a-1,o=n.alpha()-i.alpha(),s=((r*o===-1?r:(r+o)/(1+r*o))+1)/2,l=1-s;return this.rgb(s*n.red()+l*i.red(),s*n.green()+l*i.green(),s*n.blue()+l*i.blue()).alpha(n.alpha()*a+i.alpha()*(1-a))},toJSON:function(){return this.rgb()},clone:function(){var t,e,n=new r,i=this.values,a=n.values;for(var o in i)i.hasOwnProperty(o)&&(t=i[o],e={}.toString.call(t),"[object Array]"===e?a[o]=t.slice(0):"[object Number]"===e?a[o]=t:console.error("unexpected color value:",t));return n}},r.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},r.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},r.prototype.getValues=function(t){for(var e=this.values,n={},i=0;i.04045?Math.pow((e+.055)/1.055,2.4):e/12.92,n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92,i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92;var a=.4124*e+.3576*n+.1805*i,r=.2126*e+.7152*n+.0722*i,o=.0193*e+.1192*n+.9505*i;return[100*a,100*r,100*o]}function d(t){var e,n,i,a=u(t),r=a[0],o=a[1],s=a[2];return r/=95.047,o/=100,s/=108.883,r=r>.008856?Math.pow(r,1/3):7.787*r+16/116,o=o>.008856?Math.pow(o,1/3):7.787*o+16/116,s=s>.008856?Math.pow(s,1/3):7.787*s+16/116,e=116*o-16,n=500*(r-o),i=200*(o-s),[e,n,i]}function c(t){return Y(d(t))}function h(t){var e,n,i,a,r,o=t[0]/360,s=t[1]/100,l=t[2]/100;if(0==s)return r=255*l,[r,r,r];n=l<.5?l*(1+s):l+s-l*s,e=2*l-n,a=[0,0,0];for(var u=0;u<3;u++)i=o+1/3*-(u-1),i<0&&i++,i>1&&i--,r=6*i<1?e+6*(n-e)*i:2*i<1?n:3*i<2?e+(n-e)*(2/3-i)*6:e,a[u]=255*r;return a}function f(t){var e,n,i=t[0],a=t[1]/100,r=t[2]/100;return 0===r?[0,0,0]:(r*=2,a*=r<=1?r:2-r,n=(r+a)/2,e=2*a/(r+a),[i,100*e,100*n])}function p(t){return o(h(t))}function m(t){return s(h(t))}function v(t){return l(h(t))}function y(t){var e=t[0]/60,n=t[1]/100,i=t[2]/100,a=Math.floor(e)%6,r=e-Math.floor(e),o=255*i*(1-n),s=255*i*(1-n*r),l=255*i*(1-n*(1-r)),i=255*i;switch(a){case 0:return[i,l,o];case 1:return[s,i,o];case 2:return[o,i,l];case 3:return[o,s,i];case 4:return[l,o,i];case 5:return[i,o,s]}}function x(t){var e,n,i=t[0],a=t[1]/100,r=t[2]/100;return n=(2-a)*r,e=a*r,e/=n<=1?n:2-n,e=e||0,n/=2,[i,100*e,100*n]}function _(t){return o(y(t))}function k(t){return s(y(t))}function w(t){return l(y(t))}function M(t){var e,n,i,a,o=t[0]/360,s=t[1]/100,l=t[2]/100,u=s+l;switch(u>1&&(s/=u,l/=u),e=Math.floor(6*o),n=1-l,i=6*o-e,0!=(1&e)&&(i=1-i),a=s+i*(n-s),e){default:case 6:case 0:r=n,g=a,b=s;break;case 1:r=a,g=n,b=s;break;case 2:r=s,g=n,b=a;break;case 3:r=s,g=a,b=n;break;case 4:r=a,g=s,b=n;break;case 5:r=n,g=s,b=a}return[255*r,255*g,255*b]}function S(t){return i(M(t))}function D(t){return a(M(t))}function C(t){return s(M(t))}function P(t){return l(M(t))}function T(t){var e,n,i,a=t[0]/100,r=t[1]/100,o=t[2]/100,s=t[3]/100;return e=1-Math.min(1,a*(1-s)+s),n=1-Math.min(1,r*(1-s)+s),i=1-Math.min(1,o*(1-s)+s),[255*e,255*n,255*i]}function I(t){return i(T(t))}function A(t){return a(T(t))}function F(t){return o(T(t))}function O(t){return l(T(t))}function R(t){var e,n,i,a=t[0]/100,r=t[1]/100,o=t[2]/100;return e=3.2406*a+r*-1.5372+o*-.4986,n=a*-.9689+1.8758*r+.0415*o,i=.0557*a+r*-.204+1.057*o,e=e>.0031308?1.055*Math.pow(e,1/2.4)-.055:e*=12.92,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:n*=12.92,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:i*=12.92,e=Math.min(Math.max(0,e),1),n=Math.min(Math.max(0,n),1),i=Math.min(Math.max(0,i),1),[255*e,255*n,255*i]}function L(t){var e,n,i,a=t[0],r=t[1],o=t[2];return a/=95.047,r/=100,o/=108.883,a=a>.008856?Math.pow(a,1/3):7.787*a+16/116,r=r>.008856?Math.pow(r,1/3):7.787*r+16/116,o=o>.008856?Math.pow(o,1/3):7.787*o+16/116,e=116*r-16,n=500*(a-r),i=200*(r-o),[e,n,i]}function V(t){return Y(L(t))}function W(t){var e,n,i,a,r=t[0],o=t[1],s=t[2];return r<=8?(n=100*r/903.3,a=7.787*(n/100)+16/116):(n=100*Math.pow((r+16)/116,3),a=Math.pow(n/100,1/3)),e=e/95.047<=.008856?e=95.047*(o/500+a-16/116)/7.787:95.047*Math.pow(o/500+a,3),i=i/108.883<=.008859?i=108.883*(a-s/200-16/116)/7.787:108.883*Math.pow(a-s/200,3),[e,n,i]}function Y(t){var e,n,i,a=t[0],r=t[1],o=t[2];return e=Math.atan2(o,r),n=360*e/2/Math.PI,n<0&&(n+=360),i=Math.sqrt(r*r+o*o),[a,i,n]}function z(t){return R(W(t))}function N(t){var e,n,i,a=t[0],r=t[1],o=t[2];return i=o/360*2*Math.PI,e=r*Math.cos(i),n=r*Math.sin(i),[a,e,n]}function B(t){return W(N(t))}function E(t){return z(N(t))}function H(t){return J[t]}function j(t){return i(H(t))}function U(t){return a(H(t))}function G(t){return o(H(t))}function q(t){return s(H(t))}function Z(t){return d(H(t))}function X(t){return u(H(t))}e.exports={rgb2hsl:i,rgb2hsv:a,rgb2hwb:o,rgb2cmyk:s,rgb2keyword:l,rgb2xyz:u,rgb2lab:d,rgb2lch:c,hsl2rgb:h,hsl2hsv:f,hsl2hwb:p,hsl2cmyk:m,hsl2keyword:v,hsv2rgb:y,hsv2hsl:x,hsv2hwb:_,hsv2cmyk:k,hsv2keyword:w,hwb2rgb:M,hwb2hsl:S,hwb2hsv:D,hwb2cmyk:C,hwb2keyword:P,cmyk2rgb:T,cmyk2hsl:I,cmyk2hsv:A,cmyk2hwb:F,cmyk2keyword:O,keyword2rgb:H,keyword2hsl:j,keyword2hsv:U,keyword2hwb:G,keyword2cmyk:q,keyword2lab:Z,keyword2xyz:X,xyz2rgb:R,xyz2lab:L,xyz2lch:V,lab2xyz:W,lab2rgb:z,lab2lch:Y,lch2lab:N,lch2xyz:B,lch2rgb:E};var J={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},K={};for(var Q in J)K[JSON.stringify(J[Q])]=Q},{}],4:[function(t,e,n){var i=t(3),a=function(){return new u};for(var r in i){a[r+"Raw"]=function(t){return function(e){return"number"==typeof e&&(e=Array.prototype.slice.call(arguments)),i[t](e)}}(r);var o=/(\w+)2(\w+)/.exec(r),s=o[1],l=o[2];a[s]=a[s]||{},a[s][l]=a[r]=function(t){return function(e){"number"==typeof e&&(e=Array.prototype.slice.call(arguments));var n=i[t](e);if("string"==typeof n||void 0===n)return n;for(var a=0;a0)for(n=0;n0?"future":"past"];return C(n)?n(e):n.replace(/%s/i,e)}function W(t,e){var n=t.toLowerCase();Vi[n]=Vi[n+"s"]=Vi[e]=t}function Y(t){return"string"==typeof t?Vi[t]||Vi[t.toLowerCase()]:void 0}function z(t){var e,n,i={};for(n in t)c(t,n)&&(e=Y(n),e&&(i[e]=t[n]));return i}function N(t,e){Wi[t]=e}function B(t){var e=[];for(var n in t)e.push({unit:n,priority:Wi[n]});return e.sort(function(t,e){return t.priority-e.priority}),e}function E(e,n){return function(i){return null!=i?(j(this,e,i),t.updateOffset(this,n),this):H(this,e)}}function H(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function j(t,e,n){t.isValid()&&t._d["set"+(t._isUTC?"UTC":"")+e](n)}function U(t){return t=Y(t),C(this[t])?this[t]():this}function G(t,e){if("object"==typeof t){t=z(t);for(var n=B(t),i=0;i=0;return(r?n?"+":"":"-")+Math.pow(10,Math.max(0,a)).toString().substr(1)+i}function Z(t,e,n,i){var a=i;"string"==typeof i&&(a=function(){return this[i]()}),t&&(Bi[t]=a),e&&(Bi[e[0]]=function(){return q(a.apply(this,arguments),e[1],e[2])}),n&&(Bi[n]=function(){return this.localeData().ordinal(a.apply(this,arguments),t)})}function X(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function J(t){var e,n,i=t.match(Yi);for(e=0,n=i.length;e=0&&zi.test(t);)t=t.replace(zi,n),zi.lastIndex=0,i-=1;return t}function $(t,e,n){ra[t]=C(e)?e:function(t,i){return t&&n?n:e}}function tt(t,e){return c(ra,t)?ra[t](e._strict,e._locale):new RegExp(et(t))}function et(t){return nt(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,n,i,a){return e||n||i||a}))}function nt(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function it(t,e){var n,i=e;for("string"==typeof t&&(t=[t]),l(e)&&(i=function(t,n){n[e]=k(t)}),n=0;n=0&&isFinite(s.getFullYear())&&s.setFullYear(t),s}function _t(t){var e=new Date(Date.UTC.apply(null,arguments));return t<100&&t>=0&&isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t),e}function kt(t,e,n){var i=7+e-n,a=(7+_t(t,0,i).getUTCDay()-e)%7;return-a+i-1}function wt(t,e,n,i,a){var r,o,s=(7+n-i)%7,l=kt(t,i,a),u=1+7*(e-1)+s+l;return u<=0?(r=t-1,o=vt(r)+u):u>vt(t)?(r=t+1,o=u-vt(t)):(r=t,o=u),{year:r,dayOfYear:o}}function Mt(t,e,n){var i,a,r=kt(t.year(),e,n),o=Math.floor((t.dayOfYear()-r-1)/7)+1;return o<1?(a=t.year()-1,i=o+St(a,e,n)):o>St(t.year(),e,n)?(i=o-St(t.year(),e,n),a=t.year()+1):(a=t.year(),i=o),{week:i,year:a}}function St(t,e,n){var i=kt(t,e,n),a=kt(t+1,e,n);return(vt(t)-i+a)/7}function Dt(t){return Mt(t,this._week.dow,this._week.doy).week}function Ct(){return this._week.dow}function Pt(){return this._week.doy}function Tt(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function It(t){var e=Mt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")}function At(t,e){return"string"!=typeof t?t:isNaN(t)?(t=e.weekdaysParse(t),"number"==typeof t?t:null):parseInt(t,10)}function Ft(t,e){return"string"==typeof t?e.weekdaysParse(t)%7||7:isNaN(t)?null:t}function Ot(t,e){return t?a(this._weekdays)?this._weekdays[t.day()]:this._weekdays[this._weekdays.isFormat.test(e)?"format":"standalone"][t.day()]:a(this._weekdays)?this._weekdays:this._weekdays.standalone}function Rt(t){return t?this._weekdaysShort[t.day()]:this._weekdaysShort}function Lt(t){return t?this._weekdaysMin[t.day()]:this._weekdaysMin}function Vt(t,e,n){var i,a,r,o=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],i=0;i<7;++i)r=f([2e3,1]).day(i),this._minWeekdaysParse[i]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[i]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[i]=this.weekdays(r,"").toLocaleLowerCase();return n?"dddd"===e?(a=ma.call(this._weekdaysParse,o),a!==-1?a:null):"ddd"===e?(a=ma.call(this._shortWeekdaysParse,o),a!==-1?a:null):(a=ma.call(this._minWeekdaysParse,o),a!==-1?a:null):"dddd"===e?(a=ma.call(this._weekdaysParse,o),a!==-1?a:(a=ma.call(this._shortWeekdaysParse,o),a!==-1?a:(a=ma.call(this._minWeekdaysParse,o),a!==-1?a:null))):"ddd"===e?(a=ma.call(this._shortWeekdaysParse,o),a!==-1?a:(a=ma.call(this._weekdaysParse,o),a!==-1?a:(a=ma.call(this._minWeekdaysParse,o),a!==-1?a:null))):(a=ma.call(this._minWeekdaysParse,o),a!==-1?a:(a=ma.call(this._weekdaysParse,o),a!==-1?a:(a=ma.call(this._shortWeekdaysParse,o),a!==-1?a:null)))}function Wt(t,e,n){var i,a,r;if(this._weekdaysParseExact)return Vt.call(this,t,e,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),i=0;i<7;i++){if(a=f([2e3,1]).day(i),n&&!this._fullWeekdaysParse[i]&&(this._fullWeekdaysParse[i]=new RegExp("^"+this.weekdays(a,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[i]=new RegExp("^"+this.weekdaysShort(a,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[i]=new RegExp("^"+this.weekdaysMin(a,"").replace(".",".?")+"$","i")),this._weekdaysParse[i]||(r="^"+this.weekdays(a,"")+"|^"+this.weekdaysShort(a,"")+"|^"+this.weekdaysMin(a,""),this._weekdaysParse[i]=new RegExp(r.replace(".",""),"i")),n&&"dddd"===e&&this._fullWeekdaysParse[i].test(t))return i;if(n&&"ddd"===e&&this._shortWeekdaysParse[i].test(t))return i;if(n&&"dd"===e&&this._minWeekdaysParse[i].test(t))return i;if(!n&&this._weekdaysParse[i].test(t))return i}}function Yt(t){if(!this.isValid())return null!=t?this:NaN;var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=At(t,this.localeData()),this.add(t-e,"d")):e}function zt(t){if(!this.isValid())return null!=t?this:NaN;var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")}function Nt(t){if(!this.isValid())return null!=t?this:NaN;if(null!=t){var e=Ft(t,this.localeData());return this.day(this.day()%7?e:e-7)}return this.day()||7}function Bt(t){return this._weekdaysParseExact?(c(this,"_weekdaysRegex")||jt.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(c(this,"_weekdaysRegex")||(this._weekdaysRegex=Ca),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)}function Et(t){return this._weekdaysParseExact?(c(this,"_weekdaysRegex")||jt.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(c(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=Pa),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Ht(t){return this._weekdaysParseExact?(c(this,"_weekdaysRegex")||jt.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(c(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Ta),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function jt(){function t(t,e){return e.length-t.length}var e,n,i,a,r,o=[],s=[],l=[],u=[];for(e=0;e<7;e++)n=f([2e3,1]).day(e),i=this.weekdaysMin(n,""),a=this.weekdaysShort(n,""),r=this.weekdays(n,""),o.push(i),s.push(a),l.push(r),u.push(i),u.push(a),u.push(r);for(o.sort(t),s.sort(t),l.sort(t),u.sort(t),e=0;e<7;e++)s[e]=nt(s[e]),l[e]=nt(l[e]),u[e]=nt(u[e]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+o.join("|")+")","i")}function Ut(){return this.hours()%12||12}function Gt(){return this.hours()||24}function qt(t,e){Z(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function Zt(t,e){return e._meridiemParse}function Xt(t){return"p"===(t+"").toLowerCase().charAt(0)}function Jt(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"}function Kt(t){return t?t.toLowerCase().replace("_","-"):t}function Qt(t){for(var e,n,i,a,r=0;r0;){if(i=$t(a.slice(0,e).join("-")))return i;if(n&&n.length>=e&&w(a,n,!0)>=e-1)break;e--}r++}return null}function $t(t){var i=null;if(!Ra[t]&&"undefined"!=typeof n&&n&&n.exports)try{i=Ia._abbr,e("./locale/"+t),te(i)}catch(t){}return Ra[t]}function te(t,e){var n;return t&&(n=s(e)?ie(t):ee(t,e),n&&(Ia=n)),Ia._abbr}function ee(t,e){if(null!==e){var n=Oa;if(e.abbr=t,null!=Ra[t])D("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),n=Ra[t]._config;else if(null!=e.parentLocale){if(null==Ra[e.parentLocale])return La[e.parentLocale]||(La[e.parentLocale]=[]),La[e.parentLocale].push({name:t,config:e}),null;n=Ra[e.parentLocale]._config}return Ra[t]=new I(T(n,e)),La[t]&&La[t].forEach(function(t){ee(t.name,t.config)}),te(t),Ra[t]}return delete Ra[t],null}function ne(t,e){if(null!=e){var n,i=Oa;null!=Ra[t]&&(i=Ra[t]._config),e=T(i,e),n=new I(e),n.parentLocale=Ra[t],Ra[t]=n,te(t)}else null!=Ra[t]&&(null!=Ra[t].parentLocale?Ra[t]=Ra[t].parentLocale:null!=Ra[t]&&delete Ra[t]);return Ra[t]}function ie(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return Ia;if(!a(t)){if(e=$t(t))return e;t=[t]}return Qt(t)}function ae(){return Ti(Ra)}function re(t){var e,n=t._a;return n&&p(t).overflow===-2&&(e=n[la]<0||n[la]>11?la:n[ua]<1||n[ua]>ot(n[sa],n[la])?ua:n[da]<0||n[da]>24||24===n[da]&&(0!==n[ca]||0!==n[ha]||0!==n[fa])?da:n[ca]<0||n[ca]>59?ca:n[ha]<0||n[ha]>59?ha:n[fa]<0||n[fa]>999?fa:-1,p(t)._overflowDayOfYear&&(eua)&&(e=ua),p(t)._overflowWeeks&&e===-1&&(e=ga),p(t)._overflowWeekday&&e===-1&&(e=pa),p(t).overflow=e),t}function oe(t){var e,n,i,a,r,o,s=t._i,l=Va.exec(s)||Wa.exec(s);if(l){for(p(t).iso=!0,e=0,n=za.length;e10?"YYYY ":"YY "),r="HH:mm"+(n[4]?":ss":""),n[1]){var c=new Date(n[2]),h=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][c.getDay()];if(n[1].substr(0,3)!==h)return p(t).weekdayMismatch=!0,void(t._isValid=!1)}switch(n[5].length){case 2:0===l?s=" +0000":(l=d.indexOf(n[5][1].toUpperCase())-12,s=(l<0?" -":" +")+(""+l).replace(/^-?/,"0").match(/..$/)[0]+"00");break;case 4:s=u[n[5]];break;default:s=u[" GMT"]}n[5]=s,t._i=n.splice(1).join(""),o=" ZZ",t._f=i+a+r+o,fe(t),p(t).rfc2822=!0}else t._isValid=!1}function le(e){var n=Ba.exec(e._i);return null!==n?void(e._d=new Date(+n[1])):(oe(e),void(e._isValid===!1&&(delete e._isValid,se(e),e._isValid===!1&&(delete e._isValid,t.createFromInputFallback(e)))))}function ue(t,e,n){return null!=t?t:null!=e?e:n}function de(e){var n=new Date(t.now());return e._useUTC?[n.getUTCFullYear(),n.getUTCMonth(),n.getUTCDate()]:[n.getFullYear(),n.getMonth(),n.getDate()]}function ce(t){var e,n,i,a,r=[];if(!t._d){for(i=de(t),t._w&&null==t._a[ua]&&null==t._a[la]&&he(t),null!=t._dayOfYear&&(a=ue(t._a[sa],i[sa]),(t._dayOfYear>vt(a)||0===t._dayOfYear)&&(p(t)._overflowDayOfYear=!0),n=_t(a,0,t._dayOfYear),t._a[la]=n.getUTCMonth(),t._a[ua]=n.getUTCDate()),e=0;e<3&&null==t._a[e];++e)t._a[e]=r[e]=i[e];for(;e<7;e++)t._a[e]=r[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[da]&&0===t._a[ca]&&0===t._a[ha]&&0===t._a[fa]&&(t._nextDay=!0,t._a[da]=0),t._d=(t._useUTC?_t:xt).apply(null,r),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[da]=24)}}function he(t){var e,n,i,a,r,o,s,l;if(e=t._w,null!=e.GG||null!=e.W||null!=e.E)r=1,o=4,n=ue(e.GG,t._a[sa],Mt(_e(),1,4).year),i=ue(e.W,1),a=ue(e.E,1),(a<1||a>7)&&(l=!0);else{r=t._locale._week.dow,o=t._locale._week.doy;var u=Mt(_e(),r,o);n=ue(e.gg,t._a[sa],u.year),i=ue(e.w,u.week),null!=e.d?(a=e.d,(a<0||a>6)&&(l=!0)):null!=e.e?(a=e.e+r,(e.e<0||e.e>6)&&(l=!0)):a=r}i<1||i>St(n,r,o)?p(t)._overflowWeeks=!0:null!=l?p(t)._overflowWeekday=!0:(s=wt(n,i,a,r,o),t._a[sa]=s.year,t._dayOfYear=s.dayOfYear)}function fe(e){if(e._f===t.ISO_8601)return void oe(e);if(e._f===t.RFC_2822)return void se(e);e._a=[],p(e).empty=!0;var n,i,a,r,o,s=""+e._i,l=s.length,u=0;for(a=Q(e._f,e._locale).match(Yi)||[],n=0;n0&&p(e).unusedInput.push(o),s=s.slice(s.indexOf(i)+i.length),u+=i.length),Bi[r]?(i?p(e).empty=!1:p(e).unusedTokens.push(r),rt(r,i,e)):e._strict&&!i&&p(e).unusedTokens.push(r);p(e).charsLeftOver=l-u,s.length>0&&p(e).unusedInput.push(s),e._a[da]<=12&&p(e).bigHour===!0&&e._a[da]>0&&(p(e).bigHour=void 0),p(e).parsedDateParts=e._a.slice(0),p(e).meridiem=e._meridiem,e._a[da]=ge(e._locale,e._a[da],e._meridiem),ce(e),re(e)}function ge(t,e,n){var i;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?(i=t.isPM(n),i&&e<12&&(e+=12),i||12!==e||(e=0),e):e}function pe(t){var e,n,i,a,r;if(0===t._f.length)return p(t).invalidFormat=!0,void(t._d=new Date(NaN));for(a=0;athis.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Ee(){if(!s(this._isDSTShifted))return this._isDSTShifted;var t={};if(y(t,this),t=ye(t),t._a){var e=t._isUTC?f(t._a):_e(t._a);this._isDSTShifted=this.isValid()&&w(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function He(){return!!this.isValid()&&!this._isUTC}function je(){return!!this.isValid()&&this._isUTC}function Ue(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}function Ge(t,e){var n,i,a,r=t,o=null;return Te(t)?r={ms:t._milliseconds,d:t._days,M:t._months}:l(t)?(r={},e?r[e]=t:r.milliseconds=t):(o=Za.exec(t))?(n="-"===o[1]?-1:1,r={y:0,d:k(o[ua])*n,h:k(o[da])*n,m:k(o[ca])*n,s:k(o[ha])*n,ms:k(Ie(1e3*o[fa]))*n}):(o=Xa.exec(t))?(n="-"===o[1]?-1:1,r={y:qe(o[2],n),M:qe(o[3],n),w:qe(o[4],n),d:qe(o[5],n),h:qe(o[6],n),m:qe(o[7],n),s:qe(o[8],n)}):null==r?r={}:"object"==typeof r&&("from"in r||"to"in r)&&(a=Xe(_e(r.from),_e(r.to)),r={},r.ms=a.milliseconds,r.M=a.months),i=new Pe(r),Te(t)&&c(t,"_locale")&&(i._locale=t._locale),i}function qe(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Ze(t,e){var n={milliseconds:0,months:0};return n.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(n.months,"M").isAfter(e)&&--n.months,n.milliseconds=+e-+t.clone().add(n.months,"M"),n}function Xe(t,e){var n;return t.isValid()&&e.isValid()?(e=Oe(e,t),t.isBefore(e)?n=Ze(t,e):(n=Ze(e,t),n.milliseconds=-n.milliseconds,n.months=-n.months),n):{milliseconds:0,months:0}}function Je(t,e){return function(n,i){var a,r;return null===i||isNaN(+i)||(D(e,"moment()."+e+"(period, number) is deprecated. Please use moment()."+e+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),r=n,n=i,i=r),n="string"==typeof n?+n:n,a=Ge(n,i),Ke(this,a,t),this}}function Ke(e,n,i,a){var r=n._milliseconds,o=Ie(n._days),s=Ie(n._months);e.isValid()&&(a=null==a||a,r&&e._d.setTime(e._d.valueOf()+r*i),o&&j(e,"Date",H(e,"Date")+o*i),s&&ct(e,H(e,"Month")+s*i),a&&t.updateOffset(e,o||s))}function Qe(t,e){var n=t.diff(e,"days",!0);return n<-6?"sameElse":n<-1?"lastWeek":n<0?"lastDay":n<1?"sameDay":n<2?"nextDay":n<7?"nextWeek":"sameElse"}function $e(e,n){var i=e||_e(),a=Oe(i,this).startOf("day"),r=t.calendarFormat(this,a)||"sameElse",o=n&&(C(n[r])?n[r].call(this,i):n[r]);return this.format(o||this.localeData().calendar(r,this,_e(i)))}function tn(){return new b(this)}function en(t,e){var n=x(t)?t:_e(t);return!(!this.isValid()||!n.isValid())&&(e=Y(s(e)?"millisecond":e),"millisecond"===e?this.valueOf()>n.valueOf():n.valueOf()9999?K(t,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):C(Date.prototype.toISOString)?this.toDate().toISOString():K(t,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")}function hn(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var t="moment",e="";this.isLocal()||(t=0===this.utcOffset()?"moment.utc":"moment.parseZone",e="Z");var n="["+t+'("]',i=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",a="-MM-DD[T]HH:mm:ss.SSS",r=e+'[")]';return this.format(n+i+a+r)}function fn(e){e||(e=this.isUtc()?t.defaultFormatUtc:t.defaultFormat);var n=K(this,e);return this.localeData().postformat(n)}function gn(t,e){return this.isValid()&&(x(t)&&t.isValid()||_e(t).isValid())?Ge({to:this,from:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()}function pn(t){return this.from(_e(),t)}function mn(t,e){return this.isValid()&&(x(t)&&t.isValid()||_e(t).isValid())?Ge({from:this,to:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()}function vn(t){return this.to(_e(),t)}function yn(t){var e;return void 0===t?this._locale._abbr:(e=ie(t),null!=e&&(this._locale=e),this)}function bn(){return this._locale}function xn(t){switch(t=Y(t)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":case"date":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===t&&this.weekday(0),"isoWeek"===t&&this.isoWeekday(1),"quarter"===t&&this.month(3*Math.floor(this.month()/3)),this}function _n(t){return t=Y(t),void 0===t||"millisecond"===t?this:("date"===t&&(t="day"),this.startOf(t).add(1,"isoWeek"===t?"week":t).subtract(1,"ms"))}function kn(){return this._d.valueOf()-6e4*(this._offset||0)}function wn(){return Math.floor(this.valueOf()/1e3)}function Mn(){return new Date(this.valueOf())}function Sn(){var t=this;return[t.year(),t.month(),t.date(),t.hour(),t.minute(),t.second(),t.millisecond()]}function Dn(){var t=this;return{years:t.year(),months:t.month(),date:t.date(),hours:t.hours(),minutes:t.minutes(),seconds:t.seconds(),milliseconds:t.milliseconds()}}function Cn(){return this.isValid()?this.toISOString():null}function Pn(){return m(this)}function Tn(){return h({},p(this))}function In(){return p(this).overflow}function An(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}}function Fn(t,e){Z(0,[t,t.length],0,e)}function On(t){return Wn.call(this,t,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)}function Rn(t){return Wn.call(this,t,this.isoWeek(),this.isoWeekday(),1,4)}function Ln(){return St(this.year(),1,4)}function Vn(){var t=this.localeData()._week;return St(this.year(),t.dow,t.doy)}function Wn(t,e,n,i,a){var r;return null==t?Mt(this,i,a).year:(r=St(t,i,a),e>r&&(e=r),Yn.call(this,t,e,n,i,a))}function Yn(t,e,n,i,a){var r=wt(t,e,n,i,a),o=_t(r.year,0,r.dayOfYear);return this.year(o.getUTCFullYear()),this.month(o.getUTCMonth()),this.date(o.getUTCDate()),this}function zn(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)}function Nn(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")}function Bn(t,e){e[fa]=k(1e3*("0."+t))}function En(){return this._isUTC?"UTC":""}function Hn(){return this._isUTC?"Coordinated Universal Time":""}function jn(t){return _e(1e3*t)}function Un(){return _e.apply(null,arguments).parseZone()}function Gn(t){return t}function qn(t,e,n,i){var a=ie(),r=f().set(i,e);return a[n](r,t)}function Zn(t,e,n){if(l(t)&&(e=t,t=void 0),t=t||"",null!=e)return qn(t,e,n,"month");var i,a=[];for(i=0;i<12;i++)a[i]=qn(t,i,n,"month");return a}function Xn(t,e,n,i){"boolean"==typeof t?(l(e)&&(n=e,e=void 0),e=e||""):(e=t,n=e,t=!1,l(e)&&(n=e,e=void 0),e=e||"");var a=ie(),r=t?a._week.dow:0;if(null!=n)return qn(e,(n+r)%7,i,"day");var o,s=[];for(o=0;o<7;o++)s[o]=qn(e,(o+r)%7,i,"day");return s}function Jn(t,e){return Zn(t,e,"months")}function Kn(t,e){return Zn(t,e,"monthsShort")}function Qn(t,e,n){return Xn(t,e,n,"weekdays")}function $n(t,e,n){return Xn(t,e,n,"weekdaysShort")}function ti(t,e,n){return Xn(t,e,n,"weekdaysMin")}function ei(){var t=this._data;return this._milliseconds=or(this._milliseconds),this._days=or(this._days),this._months=or(this._months),t.milliseconds=or(t.milliseconds),t.seconds=or(t.seconds),t.minutes=or(t.minutes),t.hours=or(t.hours),t.months=or(t.months),t.years=or(t.years),this}function ni(t,e,n,i){var a=Ge(e,n);return t._milliseconds+=i*a._milliseconds,t._days+=i*a._days,t._months+=i*a._months,t._bubble()}function ii(t,e){return ni(this,t,e,1)}function ai(t,e){return ni(this,t,e,-1)}function ri(t){return t<0?Math.floor(t):Math.ceil(t)}function oi(){var t,e,n,i,a,r=this._milliseconds,o=this._days,s=this._months,l=this._data;return r>=0&&o>=0&&s>=0||r<=0&&o<=0&&s<=0||(r+=864e5*ri(li(s)+o),o=0,s=0),l.milliseconds=r%1e3,t=_(r/1e3),l.seconds=t%60,e=_(t/60),l.minutes=e%60,n=_(e/60),l.hours=n%24,o+=_(n/24),a=_(si(o)),s+=a,o-=ri(li(a)),i=_(s/12),s%=12,l.days=o,l.months=s,l.years=i,this}function si(t){return 4800*t/146097}function li(t){return 146097*t/4800}function ui(t){if(!this.isValid())return NaN;var e,n,i=this._milliseconds;if(t=Y(t),"month"===t||"year"===t)return e=this._days+i/864e5,n=this._months+si(e),"month"===t?n:n/12;switch(e=this._days+Math.round(li(this._months)),t){case"week":return e/7+i/6048e5;case"day":return e+i/864e5;case"hour":return 24*e+i/36e5;case"minute":return 1440*e+i/6e4;case"second":return 86400*e+i/1e3;case"millisecond":return Math.floor(864e5*e)+i;default:throw new Error("Unknown unit "+t)}}function di(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*k(this._months/12):NaN}function ci(t){return function(){return this.as(t)}}function hi(t){return t=Y(t),this.isValid()?this[t+"s"]():NaN}function fi(t){return function(){return this.isValid()?this._data[t]:NaN}}function gi(){return _(this.days()/7)}function pi(t,e,n,i,a){return a.relativeTime(e||1,!!n,t,i)}function mi(t,e,n){var i=Ge(t).abs(),a=kr(i.as("s")),r=kr(i.as("m")),o=kr(i.as("h")),s=kr(i.as("d")),l=kr(i.as("M")),u=kr(i.as("y")),d=a<=wr.ss&&["s",a]||a0,d[4]=n,pi.apply(null,d)}function vi(t){return void 0===t?kr:"function"==typeof t&&(kr=t,!0)}function yi(t,e){return void 0!==wr[t]&&(void 0===e?wr[t]:(wr[t]=e,"s"===t&&(wr.ss=e-1),!0))}function bi(t){if(!this.isValid())return this.localeData().invalidDate();var e=this.localeData(),n=mi(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)}function xi(){if(!this.isValid())return this.localeData().invalidDate();var t,e,n,i=Mr(this._milliseconds)/1e3,a=Mr(this._days),r=Mr(this._months);t=_(i/60),e=_(t/60),i%=60,t%=60,n=_(r/12),r%=12;var o=n,s=r,l=a,u=e,d=t,c=i,h=this.asSeconds();return h?(h<0?"-":"")+"P"+(o?o+"Y":"")+(s?s+"M":"")+(l?l+"D":"")+(u||d||c?"T":"")+(u?u+"H":"")+(d?d+"M":"")+(c?c+"S":""):"P0D"}var _i,ki;ki=Array.prototype.some?Array.prototype.some:function(t){for(var e=Object(this),n=e.length>>>0,i=0;i68?1900:2e3)};var ka=E("FullYear",!0);Z("w",["ww",2],"wo","week"),Z("W",["WW",2],"Wo","isoWeek"),W("week","w"),W("isoWeek","W"),N("week",5),N("isoWeek",5),$("w",qi),$("ww",qi,Hi),$("W",qi),$("WW",qi,Hi),at(["w","ww","W","WW"],function(t,e,n,i){e[i.substr(0,1)]=k(t)});var wa={dow:0,doy:6};Z("d",0,"do","day"),Z("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),Z("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),Z("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),Z("e",0,0,"weekday"),Z("E",0,0,"isoWeekday"),W("day","d"),W("weekday","e"),W("isoWeekday","E"),N("day",11),N("weekday",11),N("isoWeekday",11),$("d",qi),$("e",qi),$("E",qi),$("dd",function(t,e){return e.weekdaysMinRegex(t)}),$("ddd",function(t,e){return e.weekdaysShortRegex(t)}),$("dddd",function(t,e){return e.weekdaysRegex(t)}),at(["dd","ddd","dddd"],function(t,e,n,i){var a=n._locale.weekdaysParse(t,i,n._strict);null!=a?e.d=a:p(n).invalidWeekday=t; +}),at(["d","e","E"],function(t,e,n,i){e[i]=k(t)});var Ma="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Sa="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Da="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),Ca=aa,Pa=aa,Ta=aa;Z("H",["HH",2],0,"hour"),Z("h",["hh",2],0,Ut),Z("k",["kk",2],0,Gt),Z("hmm",0,0,function(){return""+Ut.apply(this)+q(this.minutes(),2)}),Z("hmmss",0,0,function(){return""+Ut.apply(this)+q(this.minutes(),2)+q(this.seconds(),2)}),Z("Hmm",0,0,function(){return""+this.hours()+q(this.minutes(),2)}),Z("Hmmss",0,0,function(){return""+this.hours()+q(this.minutes(),2)+q(this.seconds(),2)}),qt("a",!0),qt("A",!1),W("hour","h"),N("hour",13),$("a",Zt),$("A",Zt),$("H",qi),$("h",qi),$("k",qi),$("HH",qi,Hi),$("hh",qi,Hi),$("kk",qi,Hi),$("hmm",Zi),$("hmmss",Xi),$("Hmm",Zi),$("Hmmss",Xi),it(["H","HH"],da),it(["k","kk"],function(t,e,n){var i=k(t);e[da]=24===i?0:i}),it(["a","A"],function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t}),it(["h","hh"],function(t,e,n){e[da]=k(t),p(n).bigHour=!0}),it("hmm",function(t,e,n){var i=t.length-2;e[da]=k(t.substr(0,i)),e[ca]=k(t.substr(i)),p(n).bigHour=!0}),it("hmmss",function(t,e,n){var i=t.length-4,a=t.length-2;e[da]=k(t.substr(0,i)),e[ca]=k(t.substr(i,2)),e[ha]=k(t.substr(a)),p(n).bigHour=!0}),it("Hmm",function(t,e,n){var i=t.length-2;e[da]=k(t.substr(0,i)),e[ca]=k(t.substr(i))}),it("Hmmss",function(t,e,n){var i=t.length-4,a=t.length-2;e[da]=k(t.substr(0,i)),e[ca]=k(t.substr(i,2)),e[ha]=k(t.substr(a))});var Ia,Aa=/[ap]\.?m?\.?/i,Fa=E("Hours",!0),Oa={calendar:Ii,longDateFormat:Ai,invalidDate:Fi,ordinal:Oi,dayOfMonthOrdinalParse:Ri,relativeTime:Li,months:ya,monthsShort:ba,week:wa,weekdays:Ma,weekdaysMin:Da,weekdaysShort:Sa,meridiemParse:Aa},Ra={},La={},Va=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Wa=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ya=/Z|[+-]\d\d(?::?\d\d)?/,za=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Na=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Ba=/^\/?Date\((\-?\d+)/i,Ea=/^((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d?\d\s(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(?:\d\d)?\d\d\s)(\d\d:\d\d)(\:\d\d)?(\s(?:UT|GMT|[ECMP][SD]T|[A-IK-Za-ik-z]|[+-]\d{4}))$/;t.createFromInputFallback=S("value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))}),t.ISO_8601=function(){},t.RFC_2822=function(){};var Ha=S("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var t=_e.apply(null,arguments);return this.isValid()&&t.isValid()?tthis?this:t:v()}),Ua=function(){return Date.now?Date.now():+new Date},Ga=["year","quarter","month","week","day","hour","minute","second","millisecond"];Ae("Z",":"),Ae("ZZ",""),$("Z",na),$("ZZ",na),it(["Z","ZZ"],function(t,e,n){n._useUTC=!0,n._tzm=Fe(na,t)});var qa=/([\+\-]|\d\d)/gi;t.updateOffset=function(){};var Za=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Xa=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;Ge.fn=Pe.prototype,Ge.invalid=Ce;var Ja=Je(1,"add"),Ka=Je(-1,"subtract");t.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",t.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Qa=S("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(t){return void 0===t?this.localeData():this.locale(t)});Z(0,["gg",2],0,function(){return this.weekYear()%100}),Z(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Fn("gggg","weekYear"),Fn("ggggg","weekYear"),Fn("GGGG","isoWeekYear"),Fn("GGGGG","isoWeekYear"),W("weekYear","gg"),W("isoWeekYear","GG"),N("weekYear",1),N("isoWeekYear",1),$("G",ta),$("g",ta),$("GG",qi,Hi),$("gg",qi,Hi),$("GGGG",Ki,Ui),$("gggg",Ki,Ui),$("GGGGG",Qi,Gi),$("ggggg",Qi,Gi),at(["gggg","ggggg","GGGG","GGGGG"],function(t,e,n,i){e[i.substr(0,2)]=k(t)}),at(["gg","GG"],function(e,n,i,a){n[a]=t.parseTwoDigitYear(e)}),Z("Q",0,"Qo","quarter"),W("quarter","Q"),N("quarter",7),$("Q",Ei),it("Q",function(t,e){e[la]=3*(k(t)-1)}),Z("D",["DD",2],"Do","date"),W("date","D"),N("date",9),$("D",qi),$("DD",qi,Hi),$("Do",function(t,e){return t?e._dayOfMonthOrdinalParse||e._ordinalParse:e._dayOfMonthOrdinalParseLenient}),it(["D","DD"],ua),it("Do",function(t,e){e[ua]=k(t.match(qi)[0],10)});var $a=E("Date",!0);Z("DDD",["DDDD",3],"DDDo","dayOfYear"),W("dayOfYear","DDD"),N("dayOfYear",4),$("DDD",Ji),$("DDDD",ji),it(["DDD","DDDD"],function(t,e,n){n._dayOfYear=k(t)}),Z("m",["mm",2],0,"minute"),W("minute","m"),N("minute",14),$("m",qi),$("mm",qi,Hi),it(["m","mm"],ca);var tr=E("Minutes",!1);Z("s",["ss",2],0,"second"),W("second","s"),N("second",15),$("s",qi),$("ss",qi,Hi),it(["s","ss"],ha);var er=E("Seconds",!1);Z("S",0,0,function(){return~~(this.millisecond()/100)}),Z(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),Z(0,["SSS",3],0,"millisecond"),Z(0,["SSSS",4],0,function(){return 10*this.millisecond()}),Z(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),Z(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),Z(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),Z(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),Z(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),W("millisecond","ms"),N("millisecond",16),$("S",Ji,Ei),$("SS",Ji,Hi),$("SSS",Ji,ji);var nr;for(nr="SSSS";nr.length<=9;nr+="S")$(nr,$i);for(nr="S";nr.length<=9;nr+="S")it(nr,Bn);var ir=E("Milliseconds",!1);Z("z",0,0,"zoneAbbr"),Z("zz",0,0,"zoneName");var ar=b.prototype;ar.add=Ja,ar.calendar=$e,ar.clone=tn,ar.diff=ln,ar.endOf=_n,ar.format=fn,ar.from=gn,ar.fromNow=pn,ar.to=mn,ar.toNow=vn,ar.get=U,ar.invalidAt=In,ar.isAfter=en,ar.isBefore=nn,ar.isBetween=an,ar.isSame=rn,ar.isSameOrAfter=on,ar.isSameOrBefore=sn,ar.isValid=Pn,ar.lang=Qa,ar.locale=yn,ar.localeData=bn,ar.max=ja,ar.min=Ha,ar.parsingFlags=Tn,ar.set=G,ar.startOf=xn,ar.subtract=Ka,ar.toArray=Sn,ar.toObject=Dn,ar.toDate=Mn,ar.toISOString=cn,ar.inspect=hn,ar.toJSON=Cn,ar.toString=dn,ar.unix=wn,ar.valueOf=kn,ar.creationData=An,ar.year=ka,ar.isLeapYear=bt,ar.weekYear=On,ar.isoWeekYear=Rn,ar.quarter=ar.quarters=zn,ar.month=ht,ar.daysInMonth=ft,ar.week=ar.weeks=Tt,ar.isoWeek=ar.isoWeeks=It,ar.weeksInYear=Vn,ar.isoWeeksInYear=Ln,ar.date=$a,ar.day=ar.days=Yt,ar.weekday=zt,ar.isoWeekday=Nt,ar.dayOfYear=Nn,ar.hour=ar.hours=Fa,ar.minute=ar.minutes=tr,ar.second=ar.seconds=er,ar.millisecond=ar.milliseconds=ir,ar.utcOffset=Le,ar.utc=We,ar.local=Ye,ar.parseZone=ze,ar.hasAlignedHourOffset=Ne,ar.isDST=Be,ar.isLocal=He,ar.isUtcOffset=je,ar.isUtc=Ue,ar.isUTC=Ue,ar.zoneAbbr=En,ar.zoneName=Hn,ar.dates=S("dates accessor is deprecated. Use date instead.",$a),ar.months=S("months accessor is deprecated. Use month instead",ht),ar.years=S("years accessor is deprecated. Use year instead",ka),ar.zone=S("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Ve),ar.isDSTShifted=S("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Ee);var rr=I.prototype;rr.calendar=A,rr.longDateFormat=F,rr.invalidDate=O,rr.ordinal=R,rr.preparse=Gn,rr.postformat=Gn,rr.relativeTime=L,rr.pastFuture=V,rr.set=P,rr.months=st,rr.monthsShort=lt,rr.monthsParse=dt,rr.monthsRegex=pt,rr.monthsShortRegex=gt,rr.week=Dt,rr.firstDayOfYear=Pt,rr.firstDayOfWeek=Ct,rr.weekdays=Ot,rr.weekdaysMin=Lt,rr.weekdaysShort=Rt,rr.weekdaysParse=Wt,rr.weekdaysRegex=Bt,rr.weekdaysShortRegex=Et,rr.weekdaysMinRegex=Ht,rr.isPM=Xt,rr.meridiem=Jt,te("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10,n=1===k(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+n}}),t.lang=S("moment.lang is deprecated. Use moment.locale instead.",te),t.langData=S("moment.langData is deprecated. Use moment.localeData instead.",ie);var or=Math.abs,sr=ci("ms"),lr=ci("s"),ur=ci("m"),dr=ci("h"),cr=ci("d"),hr=ci("w"),fr=ci("M"),gr=ci("y"),pr=fi("milliseconds"),mr=fi("seconds"),vr=fi("minutes"),yr=fi("hours"),br=fi("days"),xr=fi("months"),_r=fi("years"),kr=Math.round,wr={ss:44,s:45,m:45,h:22,d:26,M:11},Mr=Math.abs,Sr=Pe.prototype;return Sr.isValid=De,Sr.abs=ei,Sr.add=ii,Sr.subtract=ai,Sr.as=ui,Sr.asMilliseconds=sr,Sr.asSeconds=lr,Sr.asMinutes=ur,Sr.asHours=dr,Sr.asDays=cr,Sr.asWeeks=hr,Sr.asMonths=fr,Sr.asYears=gr,Sr.valueOf=di,Sr._bubble=oi,Sr.get=hi,Sr.milliseconds=pr,Sr.seconds=mr,Sr.minutes=vr,Sr.hours=yr,Sr.days=br,Sr.weeks=gi,Sr.months=xr,Sr.years=_r,Sr.humanize=bi,Sr.toISOString=xi,Sr.toString=xi,Sr.toJSON=xi,Sr.locale=yn,Sr.localeData=bn,Sr.toIsoString=S("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",xi),Sr.lang=Qa,Z("X",0,0,"unix"),Z("x",0,0,"valueOf"),$("x",ta),$("X",ia),it("X",function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))}),it("x",function(t,e,n){n._d=new Date(k(t))}),t.version="2.18.1",i(_e),t.fn=ar,t.min=we,t.max=Me,t.now=Ua,t.utc=f,t.unix=jn,t.months=Jn,t.isDate=u,t.locale=te,t.invalid=v,t.duration=Ge,t.isMoment=x,t.weekdays=Qn,t.parseZone=Un,t.localeData=ie,t.isDuration=Te,t.monthsShort=Kn,t.weekdaysMin=ti,t.defineLocale=ee,t.updateLocale=ne,t.locales=ae,t.weekdaysShort=$n,t.normalizeUnits=Y,t.relativeTimeRounding=vi,t.relativeTimeThreshold=yi,t.calendarFormat=Qe,t.prototype=ar,t})},{}],7:[function(t,e,n){var i=t(28)();t(26)(i),t(40)(i),t(22)(i),t(25)(i),t(30)(i),t(21)(i),t(23)(i),t(24)(i),t(29)(i),t(32)(i),t(33)(i),t(31)(i),t(27)(i),t(34)(i),t(35)(i),t(36)(i),t(37)(i),t(38)(i),t(46)(i),t(44)(i),t(45)(i),t(47)(i),t(48)(i),t(49)(i),t(15)(i),t(16)(i),t(17)(i),t(18)(i),t(19)(i),t(20)(i),t(8)(i),t(9)(i),t(10)(i),t(11)(i),t(12)(i),t(13)(i),t(14)(i);var a=[];a.push(t(41)(i),t(42)(i),t(43)(i)),i.plugins.register(a),e.exports=i,"undefined"!=typeof window&&(window.Chart=i)},{10:10,11:11,12:12,13:13,14:14,15:15,16:16,17:17,18:18,19:19,20:20,21:21,22:22,23:23,24:24,25:25,26:26,27:27,28:28,29:29,30:30,31:31,32:32,33:33,34:34,35:35,36:36,37:37,38:38,40:40,41:41,42:42,43:43,44:44,45:45,46:46,47:47,48:48,49:49,8:8,9:9}],8:[function(t,e,n){"use strict";e.exports=function(t){t.Bar=function(e,n){return n.type="bar",new t(e,n)}}},{}],9:[function(t,e,n){"use strict";e.exports=function(t){t.Bubble=function(e,n){return n.type="bubble",new t(e,n)}}},{}],10:[function(t,e,n){"use strict";e.exports=function(t){t.Doughnut=function(e,n){return n.type="doughnut",new t(e,n)}}},{}],11:[function(t,e,n){"use strict";e.exports=function(t){t.Line=function(e,n){return n.type="line",new t(e,n)}}},{}],12:[function(t,e,n){"use strict";e.exports=function(t){t.PolarArea=function(e,n){return n.type="polarArea",new t(e,n)}}},{}],13:[function(t,e,n){"use strict";e.exports=function(t){t.Radar=function(e,n){return n.type="radar",new t(e,n)}}},{}],14:[function(t,e,n){"use strict";e.exports=function(t){var e={hover:{mode:"single"},scales:{xAxes:[{type:"linear",position:"bottom",id:"x-axis-1"}],yAxes:[{type:"linear",position:"left",id:"y-axis-1"}]},tooltips:{callbacks:{title:function(){return""},label:function(t){return"("+t.xLabel+", "+t.yLabel+")"}}}};t.defaults.scatter=e,t.controllers.scatter=t.controllers.line,t.Scatter=function(e,n){return n.type="scatter",new t(e,n)}}},{}],15:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.bar={hover:{mode:"label"},scales:{xAxes:[{type:"category",categoryPercentage:.8,barPercentage:.9,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}},t.controllers.bar=t.DatasetController.extend({dataElementType:t.elements.Rectangle,initialize:function(){var e,n=this;t.DatasetController.prototype.initialize.apply(n,arguments),e=n.getMeta(),e.stack=n.getDataset().stack,e.bar=!0},update:function(t){var e,n,i=this,a=i.getMeta().data;for(i._ruler=i.getRuler(),e=0,n=a.length;e=0&&a>0)&&(m+=a));return r=c.getPixelForValue(m),o=c.getPixelForValue(m+f),s=(o-r)/2,{size:s,base:r,head:o,center:o+s/2}},calculateBarIndexPixels:function(t,e,n){var i=this,a=n.scale,r=i.chart.isCombo,o=i.getStackIndex(t),s=a.getPixelForValue(null,e,t,r),l=n.barSize;return s-=r?n.tickSize/2:0,s+=n.fullBarSize*o,s+=n.categorySpacing/2,s+=n.barSpacing/2,{size:l,base:s,head:s+l,center:s+l/2}},draw:function(){var t,n=this,i=n.chart,a=n.getMeta().data,r=n.getDataset(),o=a.length,s=0;for(e.canvas.clipArea(i.ctx,i.chartArea);s0&&(t[0].yLabel?n=t[0].yLabel:e.labels.length>0&&t[0].index');var n=t.data,i=n.datasets,a=n.labels;if(i.length)for(var r=0;r'),a[r]&&e.push(a[r]),e.push("");return e.push(""),e.join("")},legend:{labels:{generateLabels:function(t){var n=t.data;return n.labels.length&&n.datasets.length?n.labels.map(function(i,a){var r=t.getDatasetMeta(0),o=n.datasets[0],s=r.data[a],l=s&&s.custom||{},u=e.getValueAtIndexOrDefault,d=t.options.elements.arc,c=l.backgroundColor?l.backgroundColor:u(o.backgroundColor,a,d.backgroundColor),h=l.borderColor?l.borderColor:u(o.borderColor,a,d.borderColor),f=l.borderWidth?l.borderWidth:u(o.borderWidth,a,d.borderWidth);return{text:i,fillStyle:c,strokeStyle:h,lineWidth:f,hidden:isNaN(o.data[a])||r.data[a].hidden,index:a}}):[]}},onClick:function(t,e){var n,i,a,r=e.index,o=this.chart;for(n=0,i=(o.data.datasets||[]).length;n=Math.PI?-1:g<-Math.PI?1:0);var p=g+f,m={x:Math.cos(g),y:Math.sin(g)},v={x:Math.cos(p),y:Math.sin(p)},y=g<=0&&0<=p||g<=2*Math.PI&&2*Math.PI<=p,b=g<=.5*Math.PI&&.5*Math.PI<=p||g<=2.5*Math.PI&&2.5*Math.PI<=p,x=g<=-Math.PI&&-Math.PI<=p||g<=Math.PI&&Math.PI<=p,_=g<=.5*-Math.PI&&.5*-Math.PI<=p||g<=1.5*Math.PI&&1.5*Math.PI<=p,k=h/100,w={x:x?-1:Math.min(m.x*(m.x<0?1:k),v.x*(v.x<0?1:k)),y:_?-1:Math.min(m.y*(m.y<0?1:k),v.y*(v.y<0?1:k))},M={x:y?1:Math.max(m.x*(m.x>0?1:k),v.x*(v.x>0?1:k)),y:b?1:Math.max(m.y*(m.y>0?1:k),v.y*(v.y>0?1:k))},S={width:.5*(M.x-w.x),height:.5*(M.y-w.y)};u=Math.min(s/S.width,l/S.height),d={x:(M.x+w.x)*-.5,y:(M.y+w.y)*-.5}}i.borderWidth=n.getMaxBorderWidth(c.data),i.outerRadius=Math.max((u-i.borderWidth)/2,0),i.innerRadius=Math.max(h?i.outerRadius/100*h:0,0),i.radiusLength=(i.outerRadius-i.innerRadius)/i.getVisibleDatasetCount(),i.offsetX=d.x*i.outerRadius,i.offsetY=d.y*i.outerRadius,c.total=n.calculateTotal(),n.outerRadius=i.outerRadius-i.radiusLength*n.getRingIndex(n.index),n.innerRadius=Math.max(n.outerRadius-i.radiusLength,0),e.each(c.data,function(e,i){n.updateElement(e,i,t)})},updateElement:function(t,n,i){var a=this,r=a.chart,o=r.chartArea,s=r.options,l=s.animation,u=(o.left+o.right)/2,d=(o.top+o.bottom)/2,c=s.rotation,h=s.rotation,f=a.getDataset(),g=i&&l.animateRotate?0:t.hidden?0:a.calculateCircumference(f.data[n])*(s.circumference/(2*Math.PI)),p=i&&l.animateScale?0:a.innerRadius,m=i&&l.animateScale?0:a.outerRadius,v=e.getValueAtIndexOrDefault;e.extend(t,{_datasetIndex:a.index,_index:n,_model:{x:u+r.offsetX,y:d+r.offsetY,startAngle:c,endAngle:h,circumference:g,outerRadius:m,innerRadius:p,label:v(f.label,n,r.data.labels[n])}});var y=t._model;this.removeHoverStyle(t),i&&l.animateRotate||(0===n?y.startAngle=s.rotation:y.startAngle=a.getMeta().data[n-1]._model.endAngle,y.endAngle=y.startAngle+y.circumference),t.pivot()},removeHoverStyle:function(e){t.DatasetController.prototype.removeHoverStyle.call(this,e,this.chart.options.elements.arc)},calculateTotal:function(){var t,n=this.getDataset(),i=this.getMeta(),a=0;return e.each(i.data,function(e,i){t=n.data[i],isNaN(t)||e.hidden||(a+=Math.abs(t))}),a},calculateCircumference:function(t){var e=this.getMeta().total;return e>0&&!isNaN(t)?2*Math.PI*(t/e):0},getMaxBorderWidth:function(t){for(var e,n,i=0,a=this.index,r=t.length,o=0;oi?e:i,i=n>i?n:i;return i}})}},{}],18:[function(t,e,n){"use strict";e.exports=function(t){function e(t,e){return n.getValueOrDefault(t.showLine,e.showLines)}var n=t.helpers;t.defaults.line={showLines:!0,spanGaps:!1,hover:{mode:"label"},scales:{xAxes:[{type:"category",id:"x-axis-0"}],yAxes:[{type:"linear",id:"y-axis-0"}]}},t.controllers.line=t.DatasetController.extend({datasetElementType:t.elements.Line,dataElementType:t.elements.Point,update:function(t){var i,a,r,o=this,s=o.getMeta(),l=s.dataset,u=s.data||[],d=o.chart.options,c=d.elements.line,h=o.getScaleForId(s.yAxisID),f=o.getDataset(),g=e(f,d);for(g&&(r=l.custom||{},void 0!==f.tension&&void 0===f.lineTension&&(f.lineTension=f.tension),l._scale=h,l._datasetIndex=o.index,l._children=u,l._model={spanGaps:f.spanGaps?f.spanGaps:d.spanGaps,tension:r.tension?r.tension:n.getValueOrDefault(f.lineTension,c.tension),backgroundColor:r.backgroundColor?r.backgroundColor:f.backgroundColor||c.backgroundColor,borderWidth:r.borderWidth?r.borderWidth:f.borderWidth||c.borderWidth,borderColor:r.borderColor?r.borderColor:f.borderColor||c.borderColor,borderCapStyle:r.borderCapStyle?r.borderCapStyle:f.borderCapStyle||c.borderCapStyle,borderDash:r.borderDash?r.borderDash:f.borderDash||c.borderDash,borderDashOffset:r.borderDashOffset?r.borderDashOffset:f.borderDashOffset||c.borderDashOffset,borderJoinStyle:r.borderJoinStyle?r.borderJoinStyle:f.borderJoinStyle||c.borderJoinStyle,fill:r.fill?r.fill:void 0!==f.fill?f.fill:c.fill,steppedLine:r.steppedLine?r.steppedLine:n.getValueOrDefault(f.steppedLine,c.stepped),cubicInterpolationMode:r.cubicInterpolationMode?r.cubicInterpolationMode:n.getValueOrDefault(f.cubicInterpolationMode,c.cubicInterpolationMode)},l.pivot()),i=0,a=u.length;i');var n=t.data,i=n.datasets,a=n.labels;if(i.length)for(var r=0;r'),a[r]&&e.push(a[r]),e.push("");return e.push(""),e.join("")},legend:{labels:{generateLabels:function(t){var n=t.data;return n.labels.length&&n.datasets.length?n.labels.map(function(i,a){var r=t.getDatasetMeta(0),o=n.datasets[0],s=r.data[a],l=s.custom||{},u=e.getValueAtIndexOrDefault,d=t.options.elements.arc,c=l.backgroundColor?l.backgroundColor:u(o.backgroundColor,a,d.backgroundColor),h=l.borderColor?l.borderColor:u(o.borderColor,a,d.borderColor),f=l.borderWidth?l.borderWidth:u(o.borderWidth,a,d.borderWidth);return{text:i,fillStyle:c,strokeStyle:h,lineWidth:f,hidden:isNaN(o.data[a])||r.data[a].hidden,index:a}}):[]}},onClick:function(t,e){var n,i,a,r=e.index,o=this.chart;for(n=0,i=(o.data.datasets||[]).length;n0&&!isNaN(t)?2*Math.PI/e:0}})}},{}],20:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.radar={aspectRatio:1,scale:{type:"radialLinear"},elements:{line:{tension:0}}},t.controllers.radar=t.DatasetController.extend({datasetElementType:t.elements.Line,dataElementType:t.elements.Point,linkScales:e.noop,update:function(t){var n=this,i=n.getMeta(),a=i.dataset,r=i.data,o=a.custom||{},s=n.getDataset(),l=n.chart.options.elements.line,u=n.chart.scale;void 0!==s.tension&&void 0===s.lineTension&&(s.lineTension=s.tension),e.extend(i.dataset,{_datasetIndex:n.index,_scale:u,_children:r,_loop:!0,_model:{tension:o.tension?o.tension:e.getValueOrDefault(s.lineTension,l.tension),backgroundColor:o.backgroundColor?o.backgroundColor:s.backgroundColor||l.backgroundColor,borderWidth:o.borderWidth?o.borderWidth:s.borderWidth||l.borderWidth,borderColor:o.borderColor?o.borderColor:s.borderColor||l.borderColor,fill:o.fill?o.fill:void 0!==s.fill?s.fill:l.fill,borderCapStyle:o.borderCapStyle?o.borderCapStyle:s.borderCapStyle||l.borderCapStyle,borderDash:o.borderDash?o.borderDash:s.borderDash||l.borderDash,borderDashOffset:o.borderDashOffset?o.borderDashOffset:s.borderDashOffset||l.borderDashOffset,borderJoinStyle:o.borderJoinStyle?o.borderJoinStyle:s.borderJoinStyle||l.borderJoinStyle}}),i.dataset.pivot(),e.each(r,function(e,i){n.updateElement(e,i,t)},n),n.updateBezierControlPoints()},updateElement:function(t,n,i){var a=this,r=t.custom||{},o=a.getDataset(),s=a.chart.scale,l=a.chart.options.elements.point,u=s.getPointPositionForValue(n,o.data[n]);void 0!==o.radius&&void 0===o.pointRadius&&(o.pointRadius=o.radius),void 0!==o.hitRadius&&void 0===o.pointHitRadius&&(o.pointHitRadius=o.hitRadius),e.extend(t,{_datasetIndex:a.index,_index:n,_scale:s,_model:{x:i?s.xCenter:u.x,y:i?s.yCenter:u.y,tension:r.tension?r.tension:e.getValueOrDefault(o.lineTension,a.chart.options.elements.line.tension),radius:r.radius?r.radius:e.getValueAtIndexOrDefault(o.pointRadius,n,l.radius),backgroundColor:r.backgroundColor?r.backgroundColor:e.getValueAtIndexOrDefault(o.pointBackgroundColor,n,l.backgroundColor),borderColor:r.borderColor?r.borderColor:e.getValueAtIndexOrDefault(o.pointBorderColor,n,l.borderColor),borderWidth:r.borderWidth?r.borderWidth:e.getValueAtIndexOrDefault(o.pointBorderWidth,n,l.borderWidth),pointStyle:r.pointStyle?r.pointStyle:e.getValueAtIndexOrDefault(o.pointStyle,n,l.pointStyle),hitRadius:r.hitRadius?r.hitRadius:e.getValueAtIndexOrDefault(o.pointHitRadius,n,l.hitRadius)}}),t._model.skip=r.skip?r.skip:isNaN(t._model.x)||isNaN(t._model.y)},updateBezierControlPoints:function(){var t=this.chart.chartArea,n=this.getMeta();e.each(n.data,function(i,a){var r=i._model,o=e.splineCurve(e.previousItem(n.data,a,!0)._model,r,e.nextItem(n.data,a,!0)._model,r.tension);r.controlPointPreviousX=Math.max(Math.min(o.previous.x,t.right),t.left),r.controlPointPreviousY=Math.max(Math.min(o.previous.y,t.bottom),t.top),r.controlPointNextX=Math.max(Math.min(o.next.x,t.right),t.left),r.controlPointNextY=Math.max(Math.min(o.next.y,t.bottom),t.top),i.pivot()})},setHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t.custom||{},a=t._index,r=t._model;r.radius=i.hoverRadius?i.hoverRadius:e.getValueAtIndexOrDefault(n.pointHoverRadius,a,this.chart.options.elements.point.hoverRadius),r.backgroundColor=i.hoverBackgroundColor?i.hoverBackgroundColor:e.getValueAtIndexOrDefault(n.pointHoverBackgroundColor,a,e.getHoverColor(r.backgroundColor)),r.borderColor=i.hoverBorderColor?i.hoverBorderColor:e.getValueAtIndexOrDefault(n.pointHoverBorderColor,a,e.getHoverColor(r.borderColor)),r.borderWidth=i.hoverBorderWidth?i.hoverBorderWidth:e.getValueAtIndexOrDefault(n.pointHoverBorderWidth,a,r.borderWidth)},removeHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t.custom||{},a=t._index,r=t._model,o=this.chart.options.elements.point;r.radius=i.radius?i.radius:e.getValueAtIndexOrDefault(n.pointRadius,a,o.radius),r.backgroundColor=i.backgroundColor?i.backgroundColor:e.getValueAtIndexOrDefault(n.pointBackgroundColor,a,o.backgroundColor),r.borderColor=i.borderColor?i.borderColor:e.getValueAtIndexOrDefault(n.pointBorderColor,a,o.borderColor),r.borderWidth=i.borderWidth?i.borderWidth:e.getValueAtIndexOrDefault(n.pointBorderWidth,a,o.borderWidth)}})}},{}],21:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.global.animation={duration:1e3,easing:"easeOutQuart",onProgress:e.noop,onComplete:e.noop},t.Animation=t.Element.extend({chart:null,currentStep:0,numSteps:60,easing:"",render:null,onAnimationProgress:null,onAnimationComplete:null}),t.animationService={frameDuration:17,animations:[],dropFrames:0,request:null,addAnimation:function(t,e,n,i){var a,r,o=this.animations;for(e.chart=t,i||(t.animating=!0),a=0,r=o.length;a1&&(n=Math.floor(t.dropFrames),t.dropFrames=t.dropFrames%1),t.advance(1+n);var i=Date.now();t.dropFrames+=(i-e)/t.frameDuration,t.animations.length>0&&t.requestAnimationFrame()},advance:function(t){for(var n,i,a=this.animations,r=0;r=n.numSteps?(e.callback(n.onAnimationComplete,[n],i),i.animating=!1,a.splice(r,1)):++r}},Object.defineProperty(t.Animation.prototype,"animationObject",{get:function(){return this}}),Object.defineProperty(t.Animation.prototype,"chartInstance",{get:function(){return this.chart},set:function(t){this.chart=t}})}},{}],22:[function(t,e,n){"use strict";e.exports=function(t){var e=t.canvasHelpers={};e.drawPoint=function(e,n,i,a,r){var o,s,l,u,d,c;if("object"==typeof n&&(o=n.toString(),"[object HTMLImageElement]"===o||"[object HTMLCanvasElement]"===o))return void e.drawImage(n,a-n.width/2,r-n.height/2,n.width,n.height);if(!(isNaN(i)||i<=0)){switch(n){default:e.beginPath(),e.arc(a,r,i,0,2*Math.PI),e.closePath(),e.fill();break;case"triangle":e.beginPath(),s=3*i/Math.sqrt(3),d=s*Math.sqrt(3)/2,e.moveTo(a-s/2,r+d/3),e.lineTo(a+s/2,r+d/3),e.lineTo(a,r-2*d/3),e.closePath(),e.fill();break;case"rect":c=1/Math.SQRT2*i,e.beginPath(),e.fillRect(a-c,r-c,2*c,2*c),e.strokeRect(a-c,r-c,2*c,2*c);break;case"rectRounded":var h=i/Math.SQRT2,f=a-h,g=r-h,p=Math.SQRT2*i;t.helpers.drawRoundedRectangle(e,f,g,p,p,i/2),e.fill();break;case"rectRot":c=1/Math.SQRT2*i,e.beginPath(),e.moveTo(a-c,r),e.lineTo(a,r+c),e.lineTo(a+c,r),e.lineTo(a,r-c),e.closePath(),e.fill();break;case"cross":e.beginPath(),e.moveTo(a,r+i),e.lineTo(a,r-i),e.moveTo(a-i,r),e.lineTo(a+i,r),e.closePath();break;case"crossRot":e.beginPath(),l=Math.cos(Math.PI/4)*i,u=Math.sin(Math.PI/4)*i,e.moveTo(a-l,r-u),e.lineTo(a+l,r+u),e.moveTo(a-l,r+u),e.lineTo(a+l,r-u),e.closePath();break;case"star":e.beginPath(),e.moveTo(a,r+i),e.lineTo(a,r-i),e.moveTo(a-i,r),e.lineTo(a+i,r),l=Math.cos(Math.PI/4)*i,u=Math.sin(Math.PI/4)*i,e.moveTo(a-l,r-u),e.lineTo(a+l,r+u),e.moveTo(a-l,r+u),e.lineTo(a+l,r-u),e.closePath();break;case"line":e.beginPath(),e.moveTo(a-i,r),e.lineTo(a+i,r),e.closePath();break;case"dash":e.beginPath(),e.moveTo(a,r),e.lineTo(a+i,r),e.closePath()}e.stroke()}},e.clipArea=function(t,e){t.save(),t.beginPath(),t.rect(e.left,e.top,e.right-e.left,e.bottom-e.top),t.clip()},e.unclipArea=function(t){t.restore()},e.lineTo=function(t,e,n,i){return n.steppedLine?("after"===n.steppedLine?t.lineTo(e.x,n.y):t.lineTo(n.x,e.y),void t.lineTo(n.x,n.y)):n.tension?void t.bezierCurveTo(i?e.controlPointPreviousX:e.controlPointNextX,i?e.controlPointPreviousY:e.controlPointNextY,i?n.controlPointNextX:n.controlPointPreviousX,i?n.controlPointNextY:n.controlPointPreviousY,n.x,n.y):void t.lineTo(n.x,n.y)},t.helpers.canvas=e}},{}],23:[function(t,e,n){"use strict";e.exports=function(t){function e(e){e=e||{};var n=e.data=e.data||{};return n.datasets=n.datasets||[],n.labels=n.labels||[],e.options=a.configMerge(t.defaults.global,t.defaults[e.type],e.options||{}),e}function n(t){var e=t.options;e.scale?t.scale.options=e.scale:e.scales&&e.scales.xAxes.concat(e.scales.yAxes).forEach(function(e){t.scales[e.id].options=e}),t.tooltip._options=e.tooltips}function i(t){return"top"===t||"bottom"===t}var a=t.helpers,r=t.plugins,o=t.platform;t.types={},t.instances={},t.controllers={},a.extend(t.prototype,{construct:function(n,i){var r=this;i=e(i);var s=o.acquireContext(n,i),l=s&&s.canvas,u=l&&l.height,d=l&&l.width;return r.id=a.uid(),r.ctx=s,r.canvas=l,r.config=i,r.width=d,r.height=u,r.aspectRatio=u?d/u:null,r.options=i.options,r._bufferedRender=!1,r.chart=r,r.controller=r,t.instances[r.id]=r,Object.defineProperty(r,"data",{get:function(){return r.config.data},set:function(t){r.config.data=t}}),s&&l?(r.initialize(),void r.update()):void console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return r.notify(t,"beforeInit"),a.retinaScale(t),t.bindEvents(),t.options.responsive&&t.resize(!0),t.ensureScalesHaveIDs(),t.buildScales(),t.initToolTip(),r.notify(t,"afterInit"),t},clear:function(){return a.clear(this),this},stop:function(){return t.animationService.cancelAnimation(this),this},resize:function(t){var e=this,n=e.options,i=e.canvas,o=n.maintainAspectRatio&&e.aspectRatio||null,s=Math.floor(a.getMaximumWidth(i)),l=Math.floor(o?s/o:a.getMaximumHeight(i));if((e.width!==s||e.height!==l)&&(i.width=e.width=s,i.height=e.height=l,i.style.width=s+"px",i.style.height=l+"px",a.retinaScale(e),!t)){var u={width:s,height:l};r.notify(e,"resize",[u]),e.options.onResize&&e.options.onResize(e,u),e.stop(),e.update(e.options.responsiveAnimationDuration)}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},n=t.scale;a.each(e.xAxes,function(t,e){t.id=t.id||"x-axis-"+e}),a.each(e.yAxes,function(t,e){t.id=t.id||"y-axis-"+e}),n&&(n.id=n.id||"scale")},buildScales:function(){var e=this,n=e.options,r=e.scales={},o=[];n.scales&&(o=o.concat((n.scales.xAxes||[]).map(function(t){return{options:t,dtype:"category",dposition:"bottom"}}),(n.scales.yAxes||[]).map(function(t){return{options:t,dtype:"linear",dposition:"left"}}))),n.scale&&o.push({options:n.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),a.each(o,function(n){var o=n.options,s=a.getValueOrDefault(o.type,n.dtype),l=t.scaleService.getScaleConstructor(s);if(l){i(o.position)!==i(n.dposition)&&(o.position=n.dposition);var u=new l({id:o.id,options:o,ctx:e.ctx,chart:e});r[u.id]=u,n.isDefault&&(e.scale=u)}}),t.scaleService.addScalesToLayout(this)},buildOrUpdateControllers:function(){var e=this,n=[],i=[];if(a.each(e.data.datasets,function(a,r){var o=e.getDatasetMeta(r);if(o.type||(o.type=a.type||e.config.type),n.push(o.type),o.controller)o.controller.updateIndex(r);else{var s=t.controllers[o.type];if(void 0===s)throw new Error('"'+o.type+'" is not a chart type.');o.controller=new s(e,r),i.push(o.controller)}},e),n.length>1)for(var r=1;r=0;--n)e.isDatasetVisible(n)&&e.drawDataset(n,t);r.notify(e,"afterDatasetsDraw",[t])}},drawDataset:function(t,e){var n=this,i=n.getDatasetMeta(t),a={meta:i,index:t,easingValue:e};r.notify(n,"beforeDatasetDraw",[a])!==!1&&(i.controller.draw(e),r.notify(n,"afterDatasetDraw",[a]))},getElementAtEvent:function(e){return t.Interaction.modes.single(this,e)},getElementsAtEvent:function(e){return t.Interaction.modes.label(this,e,{intersect:!0})},getElementsAtXAxis:function(e){return t.Interaction.modes["x-axis"](this,e,{intersect:!0})},getElementsAtEventForMode:function(e,n,i){var a=t.Interaction.modes[n];return"function"==typeof a?a(this,e,i):[]},getDatasetAtEvent:function(e){return t.Interaction.modes.dataset(this,e,{intersect:!0})},getDatasetMeta:function(t){var e=this,n=e.data.datasets[t];n._meta||(n._meta={});var i=n._meta[e.id];return i||(i=n._meta[e.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null}),i},getVisibleDatasetCount:function(){for(var t=0,e=0,n=this.data.datasets.length;e0||(a.forEach(function(e){delete t[e]}),delete t._chartjs)}}var i=t.helpers,a=["push","pop","shift","splice","unshift"];t.DatasetController=function(t,e){this.initialize(t,e)},i.extend(t.DatasetController.prototype,{datasetElementType:null,dataElementType:null,initialize:function(t,e){var n=this;n.chart=t,n.index=e,n.linkScales(),n.addElements()},updateIndex:function(t){this.index=t},linkScales:function(){var t=this,e=t.getMeta(),n=t.getDataset();null===e.xAxisID&&(e.xAxisID=n.xAxisID||t.chart.options.scales.xAxes[0].id),null===e.yAxisID&&(e.yAxisID=n.yAxisID||t.chart.options.scales.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(t){return this.chart.scales[t]},reset:function(){this.update(!0)},destroy:function(){this._data&&n(this._data,this)},createMetaDataset:function(){var t=this,e=t.datasetElementType;return e&&new e({_chart:t.chart,_datasetIndex:t.index})},createMetaData:function(t){var e=this,n=e.dataElementType;return n&&new n({_chart:e.chart,_datasetIndex:e.index,_index:t})},addElements:function(){var t,e,n=this,i=n.getMeta(),a=n.getDataset().data||[],r=i.data;for(t=0,e=a.length;ti&&t.insertElements(i,a-i)},insertElements:function(t,e){for(var n=0;n=0;a--)e.call(n,t[a],a);else for(a=0;a=i[n].length||!i[n][a].type?i[n].push(r.configMerge(s,e)):e.type&&e.type!==i[n][a].type?i[n][a]=r.configMerge(i[n][a],s,e):i[n][a]=r.configMerge(i[n][a],e)}):(i[n]=[],r.each(e,function(e){var a=r.getValueOrDefault(e.type,"xAxes"===n?"category":"linear");i[n].push(r.configMerge(t.scaleService.getScaleDefaults(a),e))})):i.hasOwnProperty(n)&&"object"==typeof i[n]&&null!==i[n]&&"object"==typeof e?i[n]=r.configMerge(i[n],e):i[n]=e}),i},r.getValueAtIndexOrDefault=function(t,e,n){return void 0===t||null===t?n:r.isArray(t)?e=0;i--){var a=t[i];if(e(a))return a}},r.inherits=function(t){var e=this,n=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return e.apply(this,arguments)},i=function(){this.constructor=n};return i.prototype=e.prototype,n.prototype=new i,n.extend=r.inherits,t&&r.extend(n.prototype,t),n.__super__=e.prototype,n},r.noop=function(){},r.uid=function(){var t=0;return function(){return t++}}(),r.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},r.almostEquals=function(t,e,n){return Math.abs(t-e)t},r.max=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.max(t,e)},Number.NEGATIVE_INFINITY)},r.min=function(t){return t.reduce(function(t,e){return isNaN(e)?t:Math.min(t,e)},Number.POSITIVE_INFINITY)},r.sign=Math.sign?function(t){return Math.sign(t)}:function(t){return t=+t,0===t||isNaN(t)?t:t>0?1:-1},r.log10=Math.log10?function(t){return Math.log10(t)}:function(t){return Math.log(t)/Math.LN10},r.toRadians=function(t){return t*(Math.PI/180)},r.toDegrees=function(t){return t*(180/Math.PI)},r.getAngleFromPoint=function(t,e){var n=e.x-t.x,i=e.y-t.y,a=Math.sqrt(n*n+i*i),r=Math.atan2(i,n);return r<-.5*Math.PI&&(r+=2*Math.PI),{angle:r,distance:a}},r.distanceBetweenPoints=function(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))},r.aliasPixel=function(t){return t%2===0?0:.5},r.splineCurve=function(t,e,n,i){var a=t.skip?e:t,r=e,o=n.skip?e:n,s=Math.sqrt(Math.pow(r.x-a.x,2)+Math.pow(r.y-a.y,2)),l=Math.sqrt(Math.pow(o.x-r.x,2)+Math.pow(o.y-r.y,2)),u=s/(s+l),d=l/(s+l);u=isNaN(u)?0:u,d=isNaN(d)?0:d;var c=i*u,h=i*d;return{previous:{x:r.x-c*(o.x-a.x),y:r.y-c*(o.y-a.y)},next:{x:r.x+h*(o.x-a.x),y:r.y+h*(o.y-a.y)}}},r.EPSILON=Number.EPSILON||1e-14,r.splineCurveMonotone=function(t){var e,n,i,a,o=(t||[]).map(function(t){return{model:t._model,deltaK:0,mK:0}}),s=o.length;for(e=0;e0?o[e-1]:null,a=e0?o[e-1]:null,a=e=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},r.previousItem=function(t,e,n){return n?e<=0?t[t.length-1]:t[e-1]:e<=0?t[0]:t[e-1]},r.niceNum=function(t,e){var n,i=Math.floor(r.log10(t)),a=t/Math.pow(10,i);return n=e?a<1.5?1:a<3?2:a<7?5:10:a<=1?1:a<=2?2:a<=5?5:10,n*Math.pow(10,i)};var o=r.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===(t/=1)?1:(n||(n=.3),i0?(n=l[0].clientX,i=l[0].clientY):(n=a.clientX,i=a.clientY);var u=parseFloat(r.getStyle(o,"padding-left")),d=parseFloat(r.getStyle(o,"padding-top")),c=parseFloat(r.getStyle(o,"padding-right")),h=parseFloat(r.getStyle(o,"padding-bottom")),f=s.right-s.left-u-c,g=s.bottom-s.top-d-h;return n=Math.round((n-s.left-u)/f*o.width/e.currentDevicePixelRatio),i=Math.round((i-s.top-d)/g*o.height/e.currentDevicePixelRatio),{x:n,y:i}},r.addEvent=function(t,e,n){t.addEventListener?t.addEventListener(e,n):t.attachEvent?t.attachEvent("on"+e,n):t["on"+e]=n},r.removeEvent=function(t,e,n){t.removeEventListener?t.removeEventListener(e,n,!1):t.detachEvent?t.detachEvent("on"+e,n):t["on"+e]=r.noop},r.getConstraintWidth=function(t){return a(t,"max-width","clientWidth")},r.getConstraintHeight=function(t){return a(t,"max-height","clientHeight")},r.getMaximumWidth=function(t){var e=t.parentNode,n=parseInt(r.getStyle(e,"padding-left"),10),i=parseInt(r.getStyle(e,"padding-right"),10),a=e.clientWidth-n-i,o=r.getConstraintWidth(t);return isNaN(o)?a:Math.min(a,o)},r.getMaximumHeight=function(t){var e=t.parentNode,n=parseInt(r.getStyle(e,"padding-top"),10),i=parseInt(r.getStyle(e,"padding-bottom"),10),a=e.clientHeight-n-i,o=r.getConstraintHeight(t);return isNaN(o)?a:Math.min(a,o)},r.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},r.retinaScale=function(t){var e=t.currentDevicePixelRatio=window.devicePixelRatio||1;if(1!==e){var n=t.canvas,i=t.height,a=t.width;n.height=i*e,n.width=a*e,t.ctx.scale(e,e),n.style.height=i+"px",n.style.width=a+"px"}},r.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},r.fontString=function(t,e,n){return e+" "+t+"px "+n},r.longestText=function(t,e,n,i){i=i||{};var a=i.data=i.data||{},o=i.garbageCollect=i.garbageCollect||[];i.font!==e&&(a=i.data={},o=i.garbageCollect=[],i.font=e),t.font=e;var s=0;r.each(n,function(e){void 0!==e&&null!==e&&r.isArray(e)!==!0?s=r.measureText(t,a,o,s,e):r.isArray(e)&&r.each(e,function(e){void 0===e||null===e||r.isArray(e)||(s=r.measureText(t,a,o,s,e))})});var l=o.length/2;if(l>n.length){for(var u=0;ui&&(i=r),i},r.numberOfLabelLines=function(t){var e=1;return r.each(t,function(t){r.isArray(t)&&t.length>e&&(e=t.length)}),e},r.drawRoundedRectangle=function(t,e,n,i,a,r){t.beginPath(),t.moveTo(e+r,n),t.lineTo(e+i-r,n),t.quadraticCurveTo(e+i,n,e+i,n+r),t.lineTo(e+i,n+a-r),t.quadraticCurveTo(e+i,n+a,e+i-r,n+a),t.lineTo(e+r,n+a),t.quadraticCurveTo(e,n+a,e,n+a-r),t.lineTo(e,n+r),t.quadraticCurveTo(e,n,e+r,n),t.closePath()},r.color=i?function(e){return e instanceof CanvasGradient&&(e=t.defaults.global.defaultColor),i(e)}:function(t){return console.error("Color.js not found!"),t},r.isArray=Array.isArray?function(t){return Array.isArray(t)}:function(t){return"[object Array]"===Object.prototype.toString.call(t)},r.arrayEquals=function(t,e){var n,i,a,o;if(!t||!e||t.length!==e.length)return!1;for(n=0,i=t.length;n0&&(s=t.getDatasetMeta(s[0]._datasetIndex).data),s},"x-axis":function(t,e){return r(t,e,!0)},point:function(t,n){var a=e(n,t);return i(t,a)},nearest:function(t,n,i){var r=e(n,t),o=a(t,r,i.intersect);return o.length>1&&o.sort(function(t,e){var n=t.getArea(),i=e.getArea(),a=n-i;return 0===a&&(a=t._datasetIndex-e._datasetIndex),a}),o.slice(0,1)},x:function(t,i,a){var r=e(i,t),o=[],s=!1;return n(t,function(t){t.inXRange(r.x)&&o.push(t),t.inRange(r.x,r.y)&&(s=!0)}),a.intersect&&!s&&(o=[]),o},y:function(t,i,a){var r=e(i,t),o=[],s=!1;return n(t,function(t){t.inYRange(r.y)&&o.push(t),t.inRange(r.x,r.y)&&(s=!0)}),a.intersect&&!s&&(o=[]),o}}}}},{}],28:[function(t,e,n){"use strict";e.exports=function(){var t=function(t,e){return this.construct(t,e),this};return t.defaults={global:{responsive:!0,responsiveAnimationDuration:0,maintainAspectRatio:!0,events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,defaultColor:"rgba(0,0,0,0.1)",defaultFontColor:"#666",defaultFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",defaultFontSize:12,defaultFontStyle:"normal",showLines:!0,elements:{},legendCallback:function(t){var e=[];e.push('
      ');for(var n=0;n'),t.data.datasets[n].label&&e.push(t.data.datasets[n].label),e.push("");return e.push("
    "),e.join("")}}},t.Chart=t,t}},{}],29:[function(t,e,n){"use strict";e.exports=function(t){function e(t,e){return i.where(t,function(t){return t.position===e})}function n(t,e){t.forEach(function(t,e){return t._tmpIndex_=e,t}),t.sort(function(t,n){var i=e?n:t,a=e?t:n;return i.weight===a.weight?i._tmpIndex_-a._tmpIndex_:i.weight-a.weight}),t.forEach(function(t){delete t._tmpIndex_})}var i=t.helpers;t.layoutService={defaults:{},addBox:function(t,e){t.boxes||(t.boxes=[]),e.fullWidth=e.fullWidth||!1,e.position=e.position||"top",e.weight=e.weight||0,t.boxes.push(e)},removeBox:function(t,e){var n=t.boxes?t.boxes.indexOf(e):-1;n!==-1&&t.boxes.splice(n,1)},configure:function(t,e,n){for(var i,a=["fullWidth","position","weight"],r=a.length,o=0;oc&&ot.maxHeight){o--;break}o++,d=s*u}t.labelRotation=o},afterCalculateTickRotation:function(){i.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){i.callback(this.options.beforeFit,[this])},fit:function(){var t=this,a=t.minSize={width:0,height:0},r=t.options,o=r.ticks,s=r.scaleLabel,l=r.gridLines,u=r.display,d=t.isHorizontal(),c=n(o),h=1.5*n(s).size,f=r.gridLines.tickMarkLength;if(d?a.width=t.isFullWidth()?t.maxWidth-t.margins.left-t.margins.right:t.maxWidth:a.width=u&&l.drawTicks?f:0,d?a.height=u&&l.drawTicks?f:0:a.height=t.maxHeight,s.display&&u&&(d?a.height+=h:a.width+=h),o.display&&u){var g=i.longestText(t.ctx,c.font,t.ticks,t.longestTextCache),p=i.numberOfLabelLines(t.ticks),m=.5*c.size;if(d){t.longestLabelWidth=g;var v=i.toRadians(t.labelRotation),y=Math.cos(v),b=Math.sin(v),x=b*g+c.size*p+m*p;a.height=Math.min(t.maxHeight,a.height+x),t.ctx.font=c.font;var _=t.ticks[0],k=e(t.ctx,_,c.font),w=t.ticks[t.ticks.length-1],M=e(t.ctx,w,c.font);0!==t.labelRotation?(t.paddingLeft="bottom"===r.position?y*k+3:y*m+3,t.paddingRight="bottom"===r.position?y*m+3:y*M+3):(t.paddingLeft=k/2+3,t.paddingRight=M/2+3)}else o.mirror?g=0:g+=t.options.ticks.padding,a.width=Math.min(t.maxWidth,a.width+g),t.paddingTop=c.size/2,t.paddingBottom=c.size/2}t.handleMargins(),t.width=a.width,t.height=a.height},handleMargins:function(){var t=this;t.margins&&(t.paddingLeft=Math.max(t.paddingLeft-t.margins.left,0),t.paddingTop=Math.max(t.paddingTop-t.margins.top,0),t.paddingRight=Math.max(t.paddingRight-t.margins.right,0),t.paddingBottom=Math.max(t.paddingBottom-t.margins.bottom,0))},afterFit:function(){i.callback(this.options.afterFit,[this])},isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(t){return null===t||"undefined"==typeof t?NaN:"number"!=typeof t||isFinite(t)?"object"==typeof t?t instanceof Date||t.isValid?t:this.getRightValue(this.isHorizontal()?t.x:t.y):t:NaN},getLabelForIndex:i.noop,getPixelForValue:i.noop,getValueForPixel:i.noop,getPixelForTick:function(t,e){var n=this;if(n.isHorizontal()){var i=n.width-(n.paddingLeft+n.paddingRight),a=i/Math.max(n.ticks.length-(n.options.gridLines.offsetGridLines?0:1),1),r=a*t+n.paddingLeft;e&&(r+=a/2);var o=n.left+Math.round(r);return o+=n.isFullWidth()?n.margins.left:0}var s=n.height-(n.paddingTop+n.paddingBottom);return n.top+t*(s/(n.ticks.length-1))},getPixelForDecimal:function(t){var e=this;if(e.isHorizontal()){var n=e.width-(e.paddingLeft+e.paddingRight),i=n*t+e.paddingLeft,a=e.left+Math.round(i);return a+=e.isFullWidth()?e.margins.left:0}return e.top+t*e.height},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var t=this,e=t.min,n=t.max;return t.beginAtZero?0:e<0&&n<0?n:e>0&&n>0?e:0},draw:function(e){var a=this,r=a.options;if(r.display){var o,s,l=a.ctx,u=t.defaults.global,d=r.ticks,c=r.gridLines,h=r.scaleLabel,f=0!==a.labelRotation,g=d.autoSkip,p=a.isHorizontal();d.maxTicksLimit&&(s=d.maxTicksLimit);var m=i.getValueOrDefault(d.fontColor,u.defaultFontColor),v=n(d),y=c.drawTicks?c.tickMarkLength:0,b=i.getValueOrDefault(h.fontColor,u.defaultFontColor),x=n(h),_=i.toRadians(a.labelRotation),k=Math.cos(_),w=a.longestLabelWidth*k;l.fillStyle=m;var M=[];if(p){if(o=!1,(w+d.autoSkipPadding)*a.ticks.length>a.width-(a.paddingLeft+a.paddingRight)&&(o=1+Math.floor((w+d.autoSkipPadding)*a.ticks.length/(a.width-(a.paddingLeft+a.paddingRight)))),s&&a.ticks.length>s)for(;!o||a.ticks.length/(o||1)>s;)o||(o=1),o+=1;g||(o=!1)}var S="right"===r.position?a.left:a.right-y,D="right"===r.position?a.left+y:a.right,C="bottom"===r.position?a.top:a.bottom-y,P="bottom"===r.position?a.top+y:a.bottom;if(i.each(a.ticks,function(t,n){if(void 0!==t&&null!==t){var s=a.ticks.length===n+1,l=o>1&&n%o>0||n%o===0&&n+o>=a.ticks.length;if((!l||s)&&void 0!==t&&null!==t){var h,g,m,v;n===("undefined"!=typeof a.zeroLineIndex?a.zeroLineIndex:0)?(h=c.zeroLineWidth,g=c.zeroLineColor,m=c.zeroLineBorderDash,v=c.zeroLineBorderDashOffset):(h=i.getValueAtIndexOrDefault(c.lineWidth,n),g=i.getValueAtIndexOrDefault(c.color,n),m=i.getValueOrDefault(c.borderDash,u.borderDash),v=i.getValueOrDefault(c.borderDashOffset,u.borderDashOffset));var b,x,k,w,T,I,A,F,O,R,L="middle",V="middle";if(p){"bottom"===r.position?(V=f?"middle":"top",L=f?"right":"center",R=a.top+y):(V=f?"middle":"bottom",L=f?"left":"center",R=a.bottom-y);var W=a.getPixelForTick(n)+i.aliasPixel(h);O=a.getPixelForTick(n,c.offsetGridLines)+d.labelOffset,b=k=T=A=W,x=C,w=P,I=e.top,F=e.bottom}else{var Y,z="left"===r.position,N=d.padding;d.mirror?(L=z?"left":"right",Y=N):(L=z?"right":"left",Y=y+N),O=z?a.right-Y:a.left+Y;var B=a.getPixelForTick(n);B+=i.aliasPixel(h),R=a.getPixelForTick(n,c.offsetGridLines),b=S,k=D,T=e.left,A=e.right,x=w=I=F=B}M.push({tx1:b,ty1:x,tx2:k,ty2:w,x1:T,y1:I,x2:A,y2:F,labelX:O,labelY:R,glWidth:h,glColor:g,glBorderDash:m,glBorderDashOffset:v,rotation:-1*_,label:t,textBaseline:V,textAlign:L})}}}),i.each(M,function(t){if(c.display&&(l.save(),l.lineWidth=t.glWidth,l.strokeStyle=t.glColor,l.setLineDash&&(l.setLineDash(t.glBorderDash),l.lineDashOffset=t.glBorderDashOffset),l.beginPath(),c.drawTicks&&(l.moveTo(t.tx1,t.ty1),l.lineTo(t.tx2,t.ty2)),c.drawOnChartArea&&(l.moveTo(t.x1,t.y1),l.lineTo(t.x2,t.y2)),l.stroke(),l.restore()),d.display){l.save(),l.translate(t.labelX,t.labelY),l.rotate(t.rotation),l.font=v.font,l.textBaseline=t.textBaseline,l.textAlign=t.textAlign;var e=t.label;if(i.isArray(e))for(var n=0,a=0;n0)i=t.stepSize;else{var r=e.niceNum(n.max-n.min,!1);i=e.niceNum(r/(t.maxTicks-1),!0)}var o=Math.floor(n.min/i)*i,s=Math.ceil(n.max/i)*i;t.min&&t.max&&t.stepSize&&e.almostWhole((t.max-t.min)/t.stepSize,i/1e3)&&(o=t.min,s=t.max);var l=(s-o)/i;l=e.almostEquals(l,Math.round(l),i/1e3)?Math.round(l):Math.ceil(l),a.push(void 0!==t.min?t.min:o);for(var u=1;u3?i[2]-i[1]:i[1]-i[0];Math.abs(a)>1&&t!==Math.floor(t)&&(a=t-Math.floor(t));var r=e.log10(Math.abs(a)),o="";if(0!==t){var s=-1*Math.floor(r);s=Math.max(Math.min(s,20),0),o=t.toFixed(s)}else o="0";return o},logarithmic:function(t,n,i){var a=t/Math.pow(10,Math.floor(e.log10(t)));return 0===t?"0":1===a||2===a||5===a||0===n||n===i.length-1?t.toExponential():""}}}}},{}],34:[function(t,e,n){"use strict";e.exports=function(t){function e(t,e){var n=l.color(t);return n.alpha(e*n.alpha()).rgbaString()}function n(t,e){return e&&(l.isArray(e)?Array.prototype.push.apply(t,e):t.push(e)),t}function i(t){var e=t._xScale,n=t._yScale||t._scale,i=t._index,a=t._datasetIndex;return{xLabel:e?e.getLabelForIndex(i,a):"",yLabel:n?n.getLabelForIndex(i,a):"",index:i,datasetIndex:a,x:t._model.x,y:t._model.y}}function a(e){var n=t.defaults.global,i=l.getValueOrDefault;return{xPadding:e.xPadding,yPadding:e.yPadding,xAlign:e.xAlign,yAlign:e.yAlign,bodyFontColor:e.bodyFontColor,_bodyFontFamily:i(e.bodyFontFamily,n.defaultFontFamily),_bodyFontStyle:i(e.bodyFontStyle,n.defaultFontStyle),_bodyAlign:e.bodyAlign,bodyFontSize:i(e.bodyFontSize,n.defaultFontSize),bodySpacing:e.bodySpacing,titleFontColor:e.titleFontColor,_titleFontFamily:i(e.titleFontFamily,n.defaultFontFamily),_titleFontStyle:i(e.titleFontStyle,n.defaultFontStyle),titleFontSize:i(e.titleFontSize,n.defaultFontSize),_titleAlign:e.titleAlign,titleSpacing:e.titleSpacing,titleMarginBottom:e.titleMarginBottom,footerFontColor:e.footerFontColor,_footerFontFamily:i(e.footerFontFamily,n.defaultFontFamily),_footerFontStyle:i(e.footerFontStyle,n.defaultFontStyle),footerFontSize:i(e.footerFontSize,n.defaultFontSize),_footerAlign:e.footerAlign,footerSpacing:e.footerSpacing,footerMarginTop:e.footerMarginTop,caretSize:e.caretSize,cornerRadius:e.cornerRadius,backgroundColor:e.backgroundColor,opacity:0,legendColorBackground:e.multiKeyBackground,displayColors:e.displayColors,borderColor:e.borderColor,borderWidth:e.borderWidth}}function r(t,e){var n=t._chart.ctx,i=2*e.yPadding,a=0,r=e.body,o=r.reduce(function(t,e){return t+e.before.length+e.lines.length+e.after.length},0);o+=e.beforeBody.length+e.afterBody.length;var s=e.title.length,u=e.footer.length,d=e.titleFontSize,c=e.bodyFontSize,h=e.footerFontSize;i+=s*d,i+=s?(s-1)*e.titleSpacing:0,i+=s?e.titleMarginBottom:0,i+=o*c,i+=o?(o-1)*e.bodySpacing:0,i+=u?e.footerMarginTop:0,i+=u*h,i+=u?(u-1)*e.footerSpacing:0;var f=0,g=function(t){a=Math.max(a,n.measureText(t).width+f)};return n.font=l.fontString(d,e._titleFontStyle,e._titleFontFamily),l.each(e.title,g),n.font=l.fontString(c,e._bodyFontStyle,e._bodyFontFamily),l.each(e.beforeBody.concat(e.afterBody),g),f=e.displayColors?c+2:0,l.each(r,function(t){l.each(t.before,g),l.each(t.lines,g),l.each(t.after,g)}),f=0,n.font=l.fontString(h,e._footerFontStyle,e._footerFontFamily),l.each(e.footer,g),a+=2*e.xPadding,{width:a,height:i}}function o(t,e){var n=t._model,i=t._chart,a=t._chart.chartArea,r="center",o="center";n.yi.height-e.height&&(o="bottom");var s,l,u,d,c,h=(a.left+a.right)/2,f=(a.top+a.bottom)/2;"center"===o?(s=function(t){return t<=h},l=function(t){return t>h}):(s=function(t){return t<=e.width/2},l=function(t){return t>=i.width-e.width/2}),u=function(t){return t+e.width>i.width},d=function(t){return t-e.width<0},c=function(t){return t<=f?"top":"bottom"},s(n.x)?(r="left",u(n.x)&&(r="center",o=c(n.y))):l(n.x)&&(r="right",d(n.x)&&(r="center",o=c(n.y)));var g=t._options;return{xAlign:g.xAlign?g.xAlign:r,yAlign:g.yAlign?g.yAlign:o}}function s(t,e,n){var i=t.x,a=t.y,r=t.caretSize,o=t.caretPadding,s=t.cornerRadius,l=n.xAlign,u=n.yAlign,d=r+o,c=s+o;return"right"===l?i-=e.width:"center"===l&&(i-=e.width/2),"top"===u?a+=d:a-="bottom"===u?e.height+d:e.height/2,"center"===u?"left"===l?i+=d:"right"===l&&(i-=d):"left"===l?i-=c:"right"===l&&(i+=c),{x:i,y:a}}var l=t.helpers;t.defaults.global.tooltips={enabled:!0,custom:null,mode:"nearest",position:"average",intersect:!0,backgroundColor:"rgba(0,0,0,0.8)",titleFontStyle:"bold",titleSpacing:2,titleMarginBottom:6,titleFontColor:"#fff",titleAlign:"left",bodySpacing:2,bodyFontColor:"#fff",bodyAlign:"left",footerFontStyle:"bold",footerSpacing:2,footerMarginTop:6,footerFontColor:"#fff",footerAlign:"left",yPadding:6,xPadding:6,caretPadding:2,caretSize:5,cornerRadius:6,multiKeyBackground:"#fff",displayColors:!0,borderColor:"rgba(0,0,0,0)",borderWidth:0,callbacks:{beforeTitle:l.noop,title:function(t,e){var n="",i=e.labels,a=i?i.length:0;if(t.length>0){var r=t[0];r.xLabel?n=r.xLabel:a>0&&r.index0&&i.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var n={width:e.width,height:e.height},i={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&r&&(this.drawBackground(i,e,t,n,a),i.x+=e.xPadding,i.y+=e.yPadding,this.drawTitle(i,e,t,a),this.drawBody(i,e,t,a),this.drawFooter(i,e,t,a))}},handleEvent:function(t){var e=this,n=e._options,i=!1;if(e._lastActive=e._lastActive||[],"mouseout"===t.type?e._active=[]:e._active=e._chart.getElementsAtEventForMode(t,n.mode,n),i=!l.arrayEquals(e._active,e._lastActive),!i)return!1;if(e._lastActive=e._active,n.enabled||n.custom){e._eventPosition={x:t.x,y:t.y};var a=e._model;e.update(!0),e.pivot(),i|=a.x!==e._model.x||a.y!==e._model.y}return i}}),t.Tooltip.positioners={average:function(t){if(!t.length)return!1;var e,n,i=0,a=0,r=0;for(e=0,n=t.length;el;)r-=2*Math.PI;for(;r=s&&r<=l,d=o>=i.innerRadius&&o<=i.outerRadius;return u&&d}return!1},getCenterPoint:function(){var t=this._view,e=(t.startAngle+t.endAngle)/2,n=(t.innerRadius+t.outerRadius)/2;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},getArea:function(){var t=this._view;return Math.PI*((t.endAngle-t.startAngle)/(2*Math.PI))*(Math.pow(t.outerRadius,2)-Math.pow(t.innerRadius,2))},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,n=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},draw:function(){var t=this._chart.ctx,e=this._view,n=e.startAngle,i=e.endAngle;t.beginPath(),t.arc(e.x,e.y,e.outerRadius,n,i),t.arc(e.x,e.y,e.innerRadius,i,n,!0),t.closePath(),t.strokeStyle=e.borderColor,t.lineWidth=e.borderWidth,t.fillStyle=e.backgroundColor,t.fill(),t.lineJoin="bevel",e.borderWidth&&t.stroke()}})}},{}],36:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=t.defaults.global;t.defaults.global.elements.line={tension:.4,backgroundColor:n.defaultColor,borderWidth:3,borderColor:n.defaultColor,borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",capBezierPoints:!0,fill:!0},t.elements.Line=t.Element.extend({draw:function(){var t,i,a,r,o=this,s=o._view,l=o._chart.ctx,u=s.spanGaps,d=o._children.slice(),c=n.elements.line,h=-1;for(o._loop&&d.length&&d.push(d[0]),l.save(),l.lineCap=s.borderCapStyle||c.borderCapStyle,l.setLineDash&&l.setLineDash(s.borderDash||c.borderDash),l.lineDashOffset=s.borderDashOffset||c.borderDashOffset,l.lineJoin=s.borderJoinStyle||c.borderJoinStyle,l.lineWidth=s.borderWidth||c.borderWidth,l.strokeStyle=s.borderColor||n.defaultColor,l.beginPath(),h=-1,t=0;te?1:-1,o=1,s=u.borderSkipped||"left"):(e=u.x-u.width/2,n=u.x+u.width/2,i=u.y,a=u.base,r=1,o=a>i?1:-1,s=u.borderSkipped||"bottom"),d){var c=Math.min(Math.abs(e-n),Math.abs(i-a));d=d>c?c:d;var h=d/2,f=e+("left"!==s?h*r:0),g=n+("right"!==s?-h*r:0),p=i+("top"!==s?h*o:0),m=a+("bottom"!==s?-h*o:0);f!==g&&(i=p,a=m),p!==m&&(e=f,n=g)}l.beginPath(),l.fillStyle=u.backgroundColor,l.strokeStyle=u.borderColor,l.lineWidth=d;var v=[[e,a],[e,i],[n,i],[n,a]],y=["bottom","left","top","right"],b=y.indexOf(s,0);b===-1&&(b=0);var x=t(0);l.moveTo(x[0],x[1]);for(var _=1;_<4;_++)x=t(_),l.lineTo(x[0],x[1]);l.fill(),d&&l.stroke()},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){var i=!1;if(this._view){var a=n(this);i=t>=a.left&&t<=a.right&&e>=a.top&&e<=a.bottom}return i},inLabelRange:function(t,i){var a=this;if(!a._view)return!1;var r=!1,o=n(a);return r=e(a)?t>=o.left&&t<=o.right:i>=o.top&&i<=o.bottom},inXRange:function(t){var e=n(this);return t>=e.left&&t<=e.right},inYRange:function(t){var e=n(this);return t>=e.top&&t<=e.bottom},getCenterPoint:function(){var t,n,i=this._view;return e(this)?(t=i.x,n=(i.y+i.base)/2):(t=(i.x+i.base)/2,n=i.y),{x:t,y:n}},getArea:function(){var t=this._view;return t.width*Math.abs(t.y-t.base)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}})}},{}],39:[function(t,e,n){"use strict";e.exports=function(t){function e(t,e){var n=l.getStyle(t,e),i=n&&n.match(/^(\d+)(\.\d+)?px$/);return i?Number(i[1]):void 0}function n(t,n){var i=t.style,a=t.getAttribute("height"),r=t.getAttribute("width");if(t._chartjs={initial:{height:a,width:r,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",null===r||""===r){var o=e(t,"width");void 0!==o&&(t.width=o)}if(null===a||""===a)if(""===t.style.height)t.height=t.width/(n.options.aspectRatio||2);else{var s=e(t,"height");void 0!==o&&(t.height=s)}return t}function i(t,e,n,i,a){return{type:t,chart:e,native:a||null,x:void 0!==n?n:null,y:void 0!==i?i:null}}function a(t,e){var n=u[t.type]||t.type,a=l.getRelativePosition(t,e);return i(n,e,a.x,a.y,t)}function r(t){var e=document.createElement("iframe");return e.className="chartjs-hidden-iframe",e.style.cssText="display:block;overflow:hidden;border:0;margin:0;top:0;left:0;bottom:0;right:0;height:100%;width:100%;position:absolute;pointer-events:none;z-index:-1;",e.tabIndex=-1,l.addEvent(e,"load",function(){l.addEvent(e.contentWindow||e,"resize",t),t()}),e}function o(t,e,n){var a=t._chartjs={ticking:!1},o=function(){a.ticking||(a.ticking=!0,l.requestAnimFrame.call(window,function(){if(a.resizer)return a.ticking=!1,e(i("resize",n))}))};a.resizer=r(o),t.insertBefore(a.resizer,t.firstChild)}function s(t){if(t&&t._chartjs){var e=t._chartjs.resizer;e&&(e.parentNode.removeChild(e),t._chartjs.resizer=null),delete t._chartjs}}var l=t.helpers,u={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};return{acquireContext:function(t,e){"string"==typeof t?t=document.getElementById(t):t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas);var i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(n(t,e),i):null},releaseContext:function(t){var e=t.canvas;if(e._chartjs){var n=e._chartjs.initial;["height","width"].forEach(function(t){var i=n[t];void 0===i||null===i?e.removeAttribute(t):e.setAttribute(t,i)}),l.each(n.style||{},function(t,n){e.style[n]=t}),e.width=e.width,delete e._chartjs}},addEventListener:function(t,e,n){var i=t.canvas;if("resize"===e)return void o(i.parentNode,n,t);var r=n._chartjs||(n._chartjs={}),s=r.proxies||(r.proxies={}),u=s[t.id+"_"+e]=function(e){n(a(e,t))};l.addEvent(i,e,u)},removeEventListener:function(t,e,n){var i=t.canvas;if("resize"===e)return void s(i.parentNode,n);var a=n._chartjs||{},r=a.proxies||{},o=r[t.id+"_"+e];o&&l.removeEvent(i,e,o)}}}},{}],40:[function(t,e,n){"use strict";var i=t(39);e.exports=function(t){t.platform={acquireContext:function(){},releaseContext:function(){},addEventListener:function(){},removeEventListener:function(){}},t.helpers.extend(t.platform,i(t))}},{39:39}],41:[function(t,e,n){"use strict";e.exports=function(t){function e(t,e,n){var i,a=t._model||{},r=a.fill;if(void 0===r&&(r=!!a.backgroundColor),r===!1||null===r)return!1;if(r===!0)return"origin";if(i=parseFloat(r,10),isFinite(i)&&Math.floor(i)===i)return"-"!==r[0]&&"+"!==r[0]||(i=e+i),!(i===e||i<0||i>=n)&&i;switch(r){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return r;default:return!1}}function n(t){var e,n=t.el._model||{},i=t.el._scale||{},a=t.fill,r=null;if(isFinite(a))return null;if("start"===a?r=void 0===n.scaleBottom?i.bottom:n.scaleBottom:"end"===a?r=void 0===n.scaleTop?i.top:n.scaleTop:void 0!==n.scaleZero?r=n.scaleZero:i.getBasePosition?r=i.getBasePosition():i.getBasePixel&&(r=i.getBasePixel()),void 0!==r&&null!==r){if(void 0!==r.x&&void 0!==r.y)return r;if("number"==typeof r&&isFinite(r))return e=i.isHorizontal(),{x:e?r:null,y:e?null:r}}return null}function i(t,e,n){var i,a=t[e],r=a.fill,o=[e];if(!n)return r;for(;r!==!1&&o.indexOf(r)===-1;){if(!isFinite(r))return r;if(i=t[r],!i)return!1;if(i.visible)return r;o.push(r),r=i.fill}return!1}function a(t){var e=t.fill,n="dataset";return e===!1?null:(isFinite(e)||(n="boundary"),d[n](t))}function r(t){return t&&!t.skip}function o(t,e,n,i,a){var r;if(i&&a){for(t.moveTo(e[0].x,e[0].y),r=1;r0;--r)u.canvas.lineTo(t,n[r],n[r-1],!0)}}function s(t,e,n,i,a,s){var l,u,d,c,h,f,g,p=e.length,m=i.spanGaps,v=[],y=[],b=0,x=0;for(t.beginPath(),l=0,u=p+!!s;l=n.width&&(y+=d+r.padding,v[v.length]=n.left),g[i]={left:0,top:0,width:o,height:d},v[v.length-1]+=o+r.padding}),p.height+=y}else{var b=r.padding,x=n.columnWidths=[],_=r.padding,k=0,w=0,M=d+b;i.each(n.legendItems,function(t,n){var i=e(r,d),a=i+d/2+s.measureText(t.text).width;w+M>p.height&&(_+=k+r.padding,x.push(k),k=0,w=0),k=Math.max(k,a),w+=M,g[n]={left:0,top:0,width:a,height:d}}),_+=k,x.push(k),p.width+=_}n.width=p.width,n.height=p.height},afterFit:r,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var n=this,a=n.options,r=a.labels,o=t.defaults.global,s=o.elements.line,l=n.width,u=n.lineWidths;if(a.display){var d,c=n.ctx,h=i.getValueOrDefault,f=h(r.fontColor,o.defaultFontColor),g=h(r.fontSize,o.defaultFontSize),p=h(r.fontStyle,o.defaultFontStyle),m=h(r.fontFamily,o.defaultFontFamily),v=i.fontString(g,p,m);c.textAlign="left",c.textBaseline="top",c.lineWidth=.5,c.strokeStyle=f,c.fillStyle=f,c.font=v;var y=e(r,g),b=n.legendHitBoxes,x=function(e,n,i){if(!(isNaN(y)||y<=0)){c.save(),c.fillStyle=h(i.fillStyle,o.defaultColor),c.lineCap=h(i.lineCap,s.borderCapStyle),c.lineDashOffset=h(i.lineDashOffset,s.borderDashOffset),c.lineJoin=h(i.lineJoin,s.borderJoinStyle),c.lineWidth=h(i.lineWidth,s.borderWidth),c.strokeStyle=h(i.strokeStyle,o.defaultColor);var r=0===h(i.lineWidth,s.borderWidth);if(c.setLineDash&&c.setLineDash(h(i.lineDash,s.borderDash)),a.labels&&a.labels.usePointStyle){var l=g*Math.SQRT2/2,u=l/Math.SQRT2,d=e+u,f=n+u;t.canvasHelpers.drawPoint(c,i.pointStyle,l,d,f)}else r||c.strokeRect(e,n,y,g),c.fillRect(e,n,y,g);c.restore()}},_=function(t,e,n,i){c.fillText(n.text,y+g/2+t,e),n.hidden&&(c.beginPath(),c.lineWidth=2,c.moveTo(y+g/2+t,e+g/2),c.lineTo(y+g/2+t+i,e+g/2),c.stroke())},k=n.isHorizontal();d=k?{x:n.left+(l-u[0])/2,y:n.top+r.padding,line:0}:{x:n.left+r.padding,y:n.top+r.padding,line:0};var w=g+r.padding;i.each(n.legendItems,function(t,e){var i=c.measureText(t.text).width,a=y+g/2+i,o=d.x,s=d.y;k?o+a>=l&&(s=d.y+=w,d.line++,o=d.x=n.left+(l-u[d.line])/2):s+w>n.bottom&&(o=d.x=o+n.columnWidths[d.line]+r.padding,s=d.y=n.top+r.padding,d.line++),x(o,s,t),b[e].left=o,b[e].top=s,_(o,s,t,i),k?d.x+=a+r.padding:d.y+=w})}},handleEvent:function(t){var e=this,n=e.options,i="mouseup"===t.type?"click":t.type,a=!1;if("mousemove"===i){if(!n.onHover)return}else{if("click"!==i)return;if(!n.onClick)return}var r=t.x,o=t.y;if(r>=e.left&&r<=e.right&&o>=e.top&&o<=e.bottom)for(var s=e.legendHitBoxes,l=0;l=u.left&&r<=u.left+u.width&&o>=u.top&&o<=u.top+u.height){if("click"===i){n.onClick.call(e,t.native,e.legendItems[l]),a=!0;break}if("mousemove"===i){n.onHover.call(e,t.native,e.legendItems[l]),a=!0;break}}}return a}}),{id:"legend",beforeInit:function(t){var e=t.options.legend;e&&n(t,e)},beforeUpdate:function(e){var r=e.options.legend,o=e.legend;r?(r=i.configMerge(t.defaults.global.legend,r),o?(a.configure(e,o,r),o.options=r):n(e,r)):o&&(a.removeBox(e,o),delete e.legend)},afterEvent:function(t,e){var n=t.legend;n&&n.handleEvent(e)}}}},{}],43:[function(t,e,n){"use strict";e.exports=function(t){function e(e,n){var a=new t.Title({ctx:e.ctx,options:n,chart:e});i.configure(e,a,n),i.addBox(e,a),e.titleBlock=a}var n=t.helpers,i=t.layoutService,a=n.noop;return t.defaults.global.title={display:!1,position:"top",fullWidth:!0,weight:2e3,fontStyle:"bold",padding:10,text:""},t.Title=t.Element.extend({initialize:function(t){var e=this;n.extend(e,t),e.legendHitBoxes=[]},beforeUpdate:a,update:function(t,e,n){var i=this;return i.beforeUpdate(),i.maxWidth=t,i.maxHeight=e,i.margins=n,i.beforeSetDimensions(),i.setDimensions(),i.afterSetDimensions(),i.beforeBuildLabels(),i.buildLabels(),i.afterBuildLabels(),i.beforeFit(),i.fit(),i.afterFit(),i.afterUpdate(),i.minSize},afterUpdate:a,beforeSetDimensions:a,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:a,beforeBuildLabels:a,buildLabels:a,afterBuildLabels:a,beforeFit:a,fit:function(){var e=this,i=n.getValueOrDefault,a=e.options,r=t.defaults.global,o=a.display,s=i(a.fontSize,r.defaultFontSize),l=e.minSize;e.isHorizontal()?(l.width=e.maxWidth,l.height=o?s+2*a.padding:0):(l.width=o?s+2*a.padding:0,l.height=e.maxHeight),e.width=l.width,e.height=l.height},afterFit:a,isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},draw:function(){var e=this,i=e.ctx,a=n.getValueOrDefault,r=e.options,o=t.defaults.global;if(r.display){var s,l,u,d=a(r.fontSize,o.defaultFontSize),c=a(r.fontStyle,o.defaultFontStyle),h=a(r.fontFamily,o.defaultFontFamily),f=n.fontString(d,c,h),g=0,p=e.top,m=e.left,v=e.bottom,y=e.right;i.fillStyle=a(r.fontColor,o.defaultFontColor),i.font=f,e.isHorizontal()?(s=m+(y-m)/2,l=p+(v-p)/2,u=y-m):(s="left"===r.position?m+d/2:y-d/2,l=p+(v-p)/2,u=v-p,g=Math.PI*("left"===r.position?-.5:.5)),i.save(),i.translate(s,l),i.rotate(g),i.textAlign="center",i.textBaseline="middle",i.fillText(r.text,0,0,u),i.restore()}}}),{id:"title",beforeInit:function(t){var n=t.options.title;n&&e(t,n)},beforeUpdate:function(a){var r=a.options.title,o=a.titleBlock;r?(r=n.configMerge(t.defaults.global.title,r),o?(i.configure(a,o,r),o.options=r):e(a,r)):o&&(t.layoutService.removeBox(a,o),delete a.titleBlock)}}}},{}],44:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n={position:"bottom"},i=t.Scale.extend({getLabels:function(){var t=this.chart.data;return(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels},determineDataLimits:function(){var t=this,n=t.getLabels();t.minIndex=0,t.maxIndex=n.length-1;var i;void 0!==t.options.ticks.min&&(i=e.indexOf(n,t.options.ticks.min),t.minIndex=i!==-1?i:t.minIndex),void 0!==t.options.ticks.max&&(i=e.indexOf(n,t.options.ticks.max),t.maxIndex=i!==-1?i:t.maxIndex),t.min=n[t.minIndex],t.max=n[t.maxIndex]},buildTicks:function(){var t=this,e=t.getLabels();t.ticks=0===t.minIndex&&t.maxIndex===e.length-1?e:e.slice(t.minIndex,t.maxIndex+1)},getLabelForIndex:function(t,e){var n=this,i=n.chart.data,a=n.isHorizontal();return i.yLabels&&!a?n.getRightValue(i.datasets[e].data[t]):n.ticks[t-n.minIndex]},getPixelForValue:function(t,e,n,i){var a,r=this,o=Math.max(r.maxIndex+1-r.minIndex-(r.options.gridLines.offsetGridLines?0:1),1);if(void 0!==t&&null!==t&&(a=r.isHorizontal()?t.x:t.y),void 0!==a||void 0!==t&&isNaN(e)){var s=r.getLabels();t=a||t;var l=s.indexOf(t);e=l!==-1?l:e}if(r.isHorizontal()){var u=r.width/o,d=u*(e-r.minIndex);return(r.options.gridLines.offsetGridLines&&i||r.maxIndex===r.minIndex&&i)&&(d+=u/2),r.left+Math.round(d)}var c=r.height/o,h=c*(e-r.minIndex);return r.options.gridLines.offsetGridLines&&i&&(h+=c/2),r.top+Math.round(h)},getPixelForTick:function(t,e){return this.getPixelForValue(this.ticks[t],t+this.minIndex,null,e)},getValueForPixel:function(t){var e,n=this,i=Math.max(n.ticks.length-(n.options.gridLines.offsetGridLines?0:1),1),a=n.isHorizontal(),r=(a?n.width:n.height)/i;return t-=a?n.left:n.top,n.options.gridLines.offsetGridLines&&(t-=r/2),e=t<=0?0:Math.round(t/r)},getBasePixel:function(){return this.bottom}});t.scaleService.registerScaleType("category",i,n)}},{}],45:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n={position:"left",ticks:{callback:t.Ticks.formatters.linear}},i=t.LinearScaleBase.extend({determineDataLimits:function(){function t(t){return s?t.xAxisID===n.id:t.yAxisID===n.id}var n=this,i=n.options,a=n.chart,r=a.data,o=r.datasets,s=n.isHorizontal(),l=0,u=1;n.min=null,n.max=null;var d=i.stacked;if(void 0===d&&e.each(o,function(e,n){if(!d){var i=a.getDatasetMeta(n);a.isDatasetVisible(n)&&t(i)&&void 0!==i.stack&&(d=!0)}}),i.stacked||d){var c={};e.each(o,function(r,o){var s=a.getDatasetMeta(o),l=[s.type,void 0===i.stacked&&void 0===s.stack?o:"",s.stack].join(".");void 0===c[l]&&(c[l]={positiveValues:[],negativeValues:[]});var u=c[l].positiveValues,d=c[l].negativeValues;a.isDatasetVisible(o)&&t(s)&&e.each(r.data,function(t,e){var a=+n.getRightValue(t);isNaN(a)||s.data[e].hidden||(u[e]=u[e]||0,d[e]=d[e]||0,i.relativePoints?u[e]=100:a<0?d[e]+=a:u[e]+=a)})}),e.each(c,function(t){var i=t.positiveValues.concat(t.negativeValues),a=e.min(i),r=e.max(i);n.min=null===n.min?a:Math.min(n.min,a),n.max=null===n.max?r:Math.max(n.max,r)})}else e.each(o,function(i,r){var o=a.getDatasetMeta(r);a.isDatasetVisible(r)&&t(o)&&e.each(i.data,function(t,e){var i=+n.getRightValue(t);isNaN(i)||o.data[e].hidden||(null===n.min?n.min=i:in.max&&(n.max=i))})});n.min=isFinite(n.min)?n.min:l,n.max=isFinite(n.max)?n.max:u,this.handleTickRangeOptions()},getTickLimit:function(){var n,i=this,a=i.options.ticks;if(i.isHorizontal())n=Math.min(a.maxTicksLimit?a.maxTicksLimit:11,Math.ceil(i.width/50));else{var r=e.getValueOrDefault(a.fontSize,t.defaults.global.defaultFontSize);n=Math.min(a.maxTicksLimit?a.maxTicksLimit:11,Math.ceil(i.height/(2*r)))}return n},handleDirectionalChanges:function(){this.isHorizontal()||this.ticks.reverse()},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},getPixelForValue:function(t){var e,n=this,i=n.start,a=+n.getRightValue(t),r=n.end-i;return n.isHorizontal()?(e=n.left+n.width/r*(a-i),Math.round(e)):(e=n.bottom-n.height/r*(a-i),Math.round(e))},getValueForPixel:function(t){var e=this,n=e.isHorizontal(),i=n?e.width:e.height,a=(n?t-e.left:e.bottom-t)/i;return e.start+(e.end-e.start)*a},getPixelForTick:function(t){return this.getPixelForValue(this.ticksAsNumbers[t])}});t.scaleService.registerScaleType("linear",i,n)}},{}],46:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=e.noop;t.LinearScaleBase=t.Scale.extend({handleTickRangeOptions:function(){var t=this,n=t.options,i=n.ticks;if(i.beginAtZero){var a=e.sign(t.min),r=e.sign(t.max);a<0&&r<0?t.max=0:a>0&&r>0&&(t.min=0)}void 0!==i.min?t.min=i.min:void 0!==i.suggestedMin&&(null===t.min?t.min=i.suggestedMin:t.min=Math.min(t.min,i.suggestedMin)),void 0!==i.max?t.max=i.max:void 0!==i.suggestedMax&&(null===t.max?t.max=i.suggestedMax:t.max=Math.max(t.max,i.suggestedMax)),t.min===t.max&&(t.max++,i.beginAtZero||t.min--)},getTickLimit:n,handleDirectionalChanges:n,buildTicks:function(){var n=this,i=n.options,a=i.ticks,r=n.getTickLimit();r=Math.max(2,r);var o={maxTicks:r,min:a.min,max:a.max,stepSize:e.getValueOrDefault(a.fixedStepSize,a.stepSize)},s=n.ticks=t.Ticks.generators.linear(o,n);n.handleDirectionalChanges(),n.max=e.max(s),n.min=e.min(s),a.reverse?(s.reverse(),n.start=n.max,n.end=n.min):(n.start=n.min,n.end=n.max)},convertTicksToLabels:function(){var e=this;e.ticksAsNumbers=e.ticks.slice(),e.zeroLineIndex=e.ticks.indexOf(0),t.Scale.prototype.convertTicksToLabels.call(e)}})}},{}],47:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n={position:"left",ticks:{callback:t.Ticks.formatters.logarithmic}},i=t.Scale.extend({determineDataLimits:function(){function t(t){return u?t.xAxisID===n.id:t.yAxisID===n.id}var n=this,i=n.options,a=i.ticks,r=n.chart,o=r.data,s=o.datasets,l=e.getValueOrDefault,u=n.isHorizontal();n.min=null,n.max=null,n.minNotZero=null;var d=i.stacked;if(void 0===d&&e.each(s,function(e,n){if(!d){var i=r.getDatasetMeta(n);r.isDatasetVisible(n)&&t(i)&&void 0!==i.stack&&(d=!0)}}),i.stacked||d){var c={};e.each(s,function(a,o){var s=r.getDatasetMeta(o),l=[s.type,void 0===i.stacked&&void 0===s.stack?o:"",s.stack].join(".");r.isDatasetVisible(o)&&t(s)&&(void 0===c[l]&&(c[l]=[]),e.each(a.data,function(t,e){var a=c[l],r=+n.getRightValue(t);isNaN(r)||s.data[e].hidden||(a[e]=a[e]||0,i.relativePoints?a[e]=100:a[e]+=r)}))}),e.each(c,function(t){var i=e.min(t),a=e.max(t);n.min=null===n.min?i:Math.min(n.min,i),n.max=null===n.max?a:Math.max(n.max,a)})}else e.each(s,function(i,a){var o=r.getDatasetMeta(a);r.isDatasetVisible(a)&&t(o)&&e.each(i.data,function(t,e){var i=+n.getRightValue(t);isNaN(i)||o.data[e].hidden||(null===n.min?n.min=i:in.max&&(n.max=i),0!==i&&(null===n.minNotZero||ia?{start:e-n-5,end:e}:{start:e,end:e+n+5}}function r(t){var r,o,s,l=n(t),u=Math.min(t.height/2,t.width/2),d={r:t.width,l:0,t:t.height,b:0},c={};t.ctx.font=l.font,t._pointLabelSizes=[];var h=e(t);for(r=0;rd.r&&(d.r=m.end,c.r=g),v.startd.b&&(d.b=v.end,c.b=g)}t.setReductions(u,d,c)}function o(t){var e=Math.min(t.height/2,t.width/2);t.drawingArea=Math.round(e),t.setCenterPoint(0,0,0,0)}function s(t){return 0===t||180===t?"center":t<180?"left":"right"}function l(t,e,n,i){if(f.isArray(e))for(var a=n.y,r=1.5*i,o=0;o270||t<90)&&(n.y-=e.h)}function d(t){var i=t.ctx,a=f.getValueOrDefault,r=t.options,o=r.angleLines,d=r.pointLabels;i.lineWidth=o.lineWidth,i.strokeStyle=o.color;var c=t.getDistanceFromCenterForValue(r.reverse?t.min:t.max),h=n(t);i.textBaseline="top";for(var p=e(t)-1;p>=0;p--){if(o.display){var m=t.getPointPosition(p,c);i.beginPath(),i.moveTo(t.xCenter,t.yCenter),i.lineTo(m.x,m.y),i.stroke(),i.closePath()}if(d.display){var v=t.getPointPosition(p,c+5),y=a(d.fontColor,g.defaultFontColor);i.font=h.font,i.fillStyle=y;var b=t.getIndexAngle(p),x=f.toDegrees(b);i.textAlign=s(x),u(x,t._pointLabelSizes[p],v),l(i,t.pointLabels[p]||"",v,h.size)}}}function c(t,n,i,a){var r=t.ctx;if(r.strokeStyle=f.getValueAtIndexOrDefault(n.color,a-1),r.lineWidth=f.getValueAtIndexOrDefault(n.lineWidth,a-1),t.options.gridLines.circular)r.beginPath(),r.arc(t.xCenter,t.yCenter,i,0,2*Math.PI), +r.closePath(),r.stroke();else{var o=e(t);if(0===o)return;r.beginPath();var s=t.getPointPosition(0,i);r.moveTo(s.x,s.y);for(var l=1;l0&&n>0?e:0)},draw:function(){var t=this,e=t.options,n=e.gridLines,i=e.ticks,a=f.getValueOrDefault;if(e.display){var r=t.ctx,o=a(i.fontSize,g.defaultFontSize),s=a(i.fontStyle,g.defaultFontStyle),l=a(i.fontFamily,g.defaultFontFamily),u=f.fontString(o,s,l);f.each(t.ticks,function(s,l){if(l>0||e.reverse){var d=t.getDistanceFromCenterForValue(t.ticksAsNumbers[l]),h=t.yCenter-d;if(n.display&&0!==l&&c(t,n,d,l),i.display){var f=a(i.fontColor,g.defaultFontColor);if(r.font=u,i.showLabelBackdrop){var p=r.measureText(s).width;r.fillStyle=i.backdropColor,r.fillRect(t.xCenter-p/2-i.backdropPaddingX,h-o/2-i.backdropPaddingY,p+2*i.backdropPaddingX,o+2*i.backdropPaddingY)}r.textAlign="center",r.textBaseline="middle",r.fillStyle=f,r.fillText(s,t.xCenter,h)}}}),(e.angleLines.display||e.pointLabels.display)&&d(t)}}});t.scaleService.registerScaleType("radialLinear",m,p)}},{}],49:[function(t,e,n){"use strict";var i=t(6);i="function"==typeof i?i:window.moment,e.exports=function(t){function e(t,e){var n=t.options.time;if("string"==typeof n.parser)return i(e,n.parser);if("function"==typeof n.parser)return n.parser(e);if("function"==typeof e.getMonth||"number"==typeof e)return i(e);if(e.isValid&&e.isValid())return e;var a=n.format;return"string"!=typeof a&&a.call?(console.warn("options.time.format is deprecated and replaced by options.time.parser."),a(e)):i(e,a)}function n(t,e,n,i){for(var a,r=Object.keys(s),o=r.length,l=r.indexOf(t);li;c++)l=a.steps[c],o=Math.ceil(u/(r*l));else for(;o>i&&i>0;)++l,o=Math.ceil(u/(r*l));return l}function r(t,e,n){var a=[];if(t.maxTicks){var r=t.stepSize;a.push(void 0!==t.min?t.min:n.min);for(var o=i(n.min);o.add(r,t.unit).valueOf()0&&a.add(1,"week"),a=a.valueOf()):(n=i(e.min).startOf(t.unit).valueOf(),a=i(e.max).startOf(t.unit),e.max-a>0&&a.add(1,t.unit),a=a.valueOf()),r(t,e,{min:n,max:a})};var u=t.Scale.extend({initialize:function(){if(!i)throw new Error("Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com");t.Scale.prototype.initialize.call(this)},determineDataLimits:function(){var t,n=this,i=n.options.time,a=Number.MAX_SAFE_INTEGER,r=Number.MIN_SAFE_INTEGER,s=n.chart.data,l={labels:[],datasets:[]};o.each(s.labels,function(o,s){var u=e(n,o);u.isValid()&&(i.round&&u.startOf(i.round),t=u.valueOf(),a=Math.min(t,a),r=Math.max(t,r),l.labels[s]=t)}),o.each(s.datasets,function(s,u){var d=[];"object"==typeof s.data[0]&&null!==s.data[0]&&n.chart.isDatasetVisible(u)?o.each(s.data,function(o,s){var l=e(n,n.getRightValue(o));l.isValid()&&(i.round&&l.startOf(i.round),t=l.valueOf(),a=Math.min(t,a),r=Math.max(t,r),d[s]=t)}):d=l.labels.slice(),l.datasets[u]=d}),n.dataMin=a,n.dataMax=r,n._parsedData=l},buildTicks:function(){var i,r,s=this,l=s.options.time,u=s.dataMin,d=s.dataMax;if(l.min){var c=e(s,l.min);l.round&&c.round(l.round),i=c.valueOf()}l.max&&(r=e(s,l.max).valueOf());var h=s.getLabelCapacity(i||u),f=l.unit||n(l.minUnit,i||u,r||d,h);s.displayFormat=l.displayFormats[f];var g=l.stepSize||a(i||u,r||d,f,h);s.ticks=t.Ticks.generators.time({maxTicks:h,min:i,max:r,stepSize:g,unit:f,isoWeekday:l.isoWeekday},{min:u,max:d}),s.max=o.max(s.ticks),s.min=o.min(s.ticks)},getLabelForIndex:function(t,n){var i=this,a=i.chart.data.labels&&t

    ',afterSetState:function(){},afterGetState:function(){},afterRemoveState:function(){},onStart:function(){},onEnd:function(){},onShow:function(){},onShown:function(){},onHide:function(){},onHidden:function(){},onNext:function(){},onPrev:function(){},onPause:function(){},onResume:function(){},onRedirectError:function(){}},e),this._force=!1,this._inited=!1,this._current=null,this.backdrop={overlay:null,$element:null,$background:null,backgroundShown:!1,overlayElementShown:!1}}return e.prototype.addSteps=function(t){var e,o,n;for(o=0,n=t.length;n>o;o++)e=t[o],this.addStep(e);return this},e.prototype.addStep=function(t){return this._options.steps.push(t),this},e.prototype.getStep=function(e){return null!=this._options.steps[e]?t.extend({id:"step-"+e,path:"",host:"",placement:"right",title:"",content:"

    ",next:e===this._options.steps.length-1?-1:e+1,prev:e-1,animation:!0,container:this._options.container,autoscroll:this._options.autoscroll,backdrop:this._options.backdrop,backdropContainer:this._options.backdropContainer,backdropPadding:this._options.backdropPadding,redirect:this._options.redirect,reflexElement:this._options.steps[e].element,backdropElement:this._options.steps[e].element,orphan:this._options.orphan,duration:this._options.duration,delay:this._options.delay,template:this._options.template,onShow:this._options.onShow,onShown:this._options.onShown,onHide:this._options.onHide,onHidden:this._options.onHidden,onNext:this._options.onNext,onPrev:this._options.onPrev,onPause:this._options.onPause,onResume:this._options.onResume,onRedirectError:this._options.onRedirectError},this._options.steps[e]):void 0},e.prototype.init=function(t){return this._force=t,this.ended()?(this._debug("Tour ended, init prevented."),this):(this.setCurrentStep(),this._initMouseNavigation(),this._initKeyboardNavigation(),this._onResize(function(t){return function(){return t.showStep(t._current)}}(this)),null!==this._current&&this.showStep(this._current),this._inited=!0,this)},e.prototype.start=function(t){var e;return null==t&&(t=!1),this._inited||this.init(t),null===this._current&&(e=this._makePromise(null!=this._options.onStart?this._options.onStart(this):void 0),this._callOnPromiseDone(e,this.showStep,0)),this},e.prototype.next=function(){var t;return t=this.hideStep(this._current,this._current+1),this._callOnPromiseDone(t,this._showNextStep)},e.prototype.prev=function(){var t;return t=this.hideStep(this._current,this._current-1),this._callOnPromiseDone(t,this._showPrevStep)},e.prototype.goTo=function(t){var e;return e=this.hideStep(this._current,t),this._callOnPromiseDone(e,this.showStep,t)},e.prototype.end=function(){var e,n;return e=function(e){return function(){return t(o).off("click.tour-"+e._options.name),t(o).off("keyup.tour-"+e._options.name),t(window).off("resize.tour-"+e._options.name),e._setState("end","yes"),e._inited=!1,e._force=!1,e._clearTimer(),null!=e._options.onEnd?e._options.onEnd(e):void 0}}(this),n=this.hideStep(this._current),this._callOnPromiseDone(n,e)},e.prototype.ended=function(){return!this._force&&!!this._getState("end")},e.prototype.restart=function(){return this._removeState("current_step"),this._removeState("end"),this._removeState("redirect_to"),this.start()},e.prototype.pause=function(){var t;return t=this.getStep(this._current),t&&t.duration?(this._paused=!0,this._duration-=(new Date).getTime()-this._start,window.clearTimeout(this._timer),this._debug("Paused/Stopped step "+(this._current+1)+" timer ("+this._duration+" remaining)."),null!=t.onPause?t.onPause(this,this._duration):void 0):this},e.prototype.resume=function(){var t;return t=this.getStep(this._current),t&&t.duration?(this._paused=!1,this._start=(new Date).getTime(),this._duration=this._duration||t.duration,this._timer=window.setTimeout(function(t){return function(){return t._isLast()?t.next():t.end()}}(this),this._duration),this._debug("Started step "+(this._current+1)+" timer with duration "+this._duration),null!=t.onResume&&this._duration!==t.duration?t.onResume(this,this._duration):void 0):this},e.prototype.hideStep=function(e,o){var n,r,i,s;return(s=this.getStep(e))?(this._clearTimer(),i=this._makePromise(null!=s.onHide?s.onHide(this,e):void 0),r=function(n){return function(){var r,i;return r=t(s.element),r.data("bs.popover")||r.data("popover")||(r=t("body")),r.popover("destroy").removeClass("tour-"+n._options.name+"-element tour-"+n._options.name+"-"+e+"-element").removeData("bs.popover").focus(),s.reflex&&t(s.reflexElement).removeClass("tour-step-element-reflex").off(""+n._reflexEvent(s.reflex)+".tour-"+n._options.name),s.backdrop&&(i=null!=o&&n.getStep(o),i&&i.backdrop&&i.backdropElement===s.backdropElement||n._hideBackdrop()),null!=s.onHidden?s.onHidden(n):void 0}}(this),n=s.delay.hide||s.delay,"[object Number]"==={}.toString.call(n)&&n>0?(this._debug("Wait "+n+" milliseconds to hide the step "+(this._current+1)),window.setTimeout(function(t){return function(){return t._callOnPromiseDone(i,r)}}(this),n)):this._callOnPromiseDone(i,r),i):void 0},e.prototype.showStep=function(t){var e,n,r,i,s,a;return this.ended()?(this._debug("Tour ended, showStep prevented."),this):(a=this.getStep(t),a&&(s=t0?(this._debug("Wait "+r+" milliseconds to show the step "+(this._current+1)),window.setTimeout(function(t){return function(){return t._callOnPromiseDone(n,i)}}(this),r)):this._callOnPromiseDone(n,i),n):void 0)},e.prototype.getCurrentStep=function(){return this._current},e.prototype.setCurrentStep=function(t){return null!=t?(this._current=t,this._setState("current_step",t)):(this._current=this._getState("current_step"),this._current=null===this._current?null:parseInt(this._current,10)),this},e.prototype.redraw=function(){return this._showOverlayElement(this.getStep(this.getCurrentStep()).element,!0)},e.prototype._setState=function(t,e){var o,n;if(this._options.storage){n=""+this._options.name+"_"+t;try{this._options.storage.setItem(n,e)}catch(r){o=r,o.code===DOMException.QUOTA_EXCEEDED_ERR&&this._debug("LocalStorage quota exceeded. State storage failed.")}return this._options.afterSetState(n,e)}return null==this._state&&(this._state={}),this._state[t]=e},e.prototype._removeState=function(t){var e;return this._options.storage?(e=""+this._options.name+"_"+t,this._options.storage.removeItem(e),this._options.afterRemoveState(e)):null!=this._state?delete this._state[t]:void 0},e.prototype._getState=function(t){var e,o;return this._options.storage?(e=""+this._options.name+"_"+t,o=this._options.storage.getItem(e)):null!=this._state&&(o=this._state[t]),(void 0===o||"null"===o)&&(o=null),this._options.afterGetState(t,o),o},e.prototype._showNextStep=function(){var t,e,o;return o=this.getStep(this._current),e=function(t){return function(){return t.showStep(o.next)}}(this),t=this._makePromise(null!=o.onNext?o.onNext(this):void 0),this._callOnPromiseDone(t,e)},e.prototype._showPrevStep=function(){var t,e,o;return o=this.getStep(this._current),e=function(t){return function(){return t.showStep(o.prev)}}(this),t=this._makePromise(null!=o.onPrev?o.onPrev(this):void 0),this._callOnPromiseDone(t,e)},e.prototype._debug=function(t){return this._options.debug?window.console.log("Bootstrap Tour '"+this._options.name+"' | "+t):void 0},e.prototype._isRedirect=function(t,e,o){var n;return null!=t&&""!==t&&("[object RegExp]"==={}.toString.call(t)&&!t.test(o.origin)||"[object String]"==={}.toString.call(t)&&this._isHostDifferent(t,o))?!0:(n=[o.pathname,o.search,o.hash].join(""),null!=e&&""!==e&&("[object RegExp]"==={}.toString.call(e)&&!e.test(n)||"[object String]"==={}.toString.call(e)&&this._isPathDifferent(e,n)))},e.prototype._isHostDifferent=function(t,e){switch({}.toString.call(t)){case"[object RegExp]":return!t.test(e.origin);case"[object String]":return this._getProtocol(t)!==this._getProtocol(e.href)||this._getHost(t)!==this._getHost(e.href);default:return!0}},e.prototype._isPathDifferent=function(t,e){return this._getPath(t)!==this._getPath(e)||!this._equal(this._getQuery(t),this._getQuery(e))||!this._equal(this._getHash(t),this._getHash(e))},e.prototype._isJustPathHashDifferent=function(t,e,o){var n;return null!=t&&""!==t&&this._isHostDifferent(t,o)?!1:(n=[o.pathname,o.search,o.hash].join(""),"[object String]"==={}.toString.call(e)?this._getPath(e)===this._getPath(n)&&this._equal(this._getQuery(e),this._getQuery(n))&&!this._equal(this._getHash(e),this._getHash(n)):!1)},e.prototype._redirect=function(e,n,r){var i;return t.isFunction(e.redirect)?e.redirect.call(this,r):(i="[object String]"==={}.toString.call(e.host)?""+e.host+r:r,this._debug("Redirect to "+i),this._getState("redirect_to")!==""+n?(this._setState("redirect_to",""+n),o.location.href=i):(this._debug("Error redirection loop to "+r),this._removeState("redirect_to"),null!=e.onRedirectError?e.onRedirectError(this):void 0))},e.prototype._isOrphan=function(e){return null==e.element||!t(e.element).length||t(e.element).is(":hidden")&&"http://www.w3.org/2000/svg"!==t(e.element)[0].namespaceURI},e.prototype._isLast=function(){return this._current").parent().html()},e.prototype._reflexEvent=function(t){return"[object Boolean]"==={}.toString.call(t)?"click":t},e.prototype._focus=function(t,e,o){var n,r;return r=o?"end":"next",n=t.find("[data-role='"+r+"']"),e.on("shown.bs.popover",function(){return n.focus()})},e.prototype._reposition=function(e,n){var r,i,s,a,u,p,h;if(a=e[0].offsetWidth,i=e[0].offsetHeight,h=e.offset(),u=h.left,p=h.top,r=t(o).outerHeight()-h.top-e.outerHeight(),0>r&&(h.top=h.top+r),s=t("html").outerWidth()-h.left-e.outerWidth(),0>s&&(h.left=h.left+s),h.top<0&&(h.top=0),h.left<0&&(h.left=0),e.offset(h),"bottom"===n.placement||"top"===n.placement){if(u!==h.left)return this._replaceArrow(e,2*(h.left-u),a,"left")}else if(p!==h.top)return this._replaceArrow(e,2*(h.top-p),i,"top")},e.prototype._center=function(e){return e.css("top",t(window).outerHeight()/2-e.outerHeight()/2)},e.prototype._replaceArrow=function(t,e,o,n){return t.find(".arrow").css(n,e?50*(1-e/o)+"%":"")},e.prototype._scrollIntoView=function(e,o){var n,r,i,s,a,u,p;if(n=t(e.element),!n.length)return o();switch(r=t(window),a=n.offset().top,s=n.outerHeight(),p=r.height(),u=0,e.placement){case"top":u=Math.max(0,a-p/2);break;case"left":case"right":u=Math.max(0,a+s/2-p/2);break;case"bottom":u=Math.max(0,a+s-p/2)}return this._debug("Scroll into view. ScrollTop: "+u+". Element offset: "+a+". Window height: "+p+"."),i=0,t("body, html").stop(!0,!0).animate({scrollTop:Math.ceil(u)},function(t){return function(){return 2===++i?(o(),t._debug("Scroll into view.\nAnimation end element offset: "+n.offset().top+".\nWindow height: "+r.height()+".")):void 0}}(this))},e.prototype._onResize=function(e,o){return t(window).on("resize.tour-"+this._options.name,function(){return clearTimeout(o),o=setTimeout(e,100)})},e.prototype._initMouseNavigation=function(){var e;return e=this,t(o).off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='prev']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='next']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='end']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='pause-resume']").on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='next']",function(t){return function(e){return e.preventDefault(),t.next()}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='prev']",function(t){return function(e){return e.preventDefault(),t._current>0?t.prev():void 0}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='end']",function(t){return function(e){return e.preventDefault(),t.end()}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='pause-resume']",function(o){var n;return o.preventDefault(),n=t(this),n.text(e._paused?n.data("pause-text"):n.data("resume-text")),e._paused?e.resume():e.pause()})},e.prototype._initKeyboardNavigation=function(){return this._options.keyboard?t(o).on("keyup.tour-"+this._options.name,function(t){return function(e){if(e.which)switch(e.which){case 39:return e.preventDefault(),t._isLast()?t.next():t.end();case 37:if(e.preventDefault(),t._current>0)return t.prev()}}}(this)):void 0},e.prototype._makePromise=function(e){return e&&t.isFunction(e.then)?e:null},e.prototype._callOnPromiseDone=function(t,e,o){return t?t.then(function(t){return function(){return e.call(t,o)}}(this)):e.call(this,o)},e.prototype._showBackdrop=function(e){return this.backdrop.backgroundShown?void 0:(this.backdrop=t("
    ",{"class":"tour-backdrop"}),this.backdrop.backgroundShown=!0,t(e.backdropContainer).append(this.backdrop))},e.prototype._hideBackdrop=function(){return this._hideOverlayElement(),this._hideBackground()},e.prototype._hideBackground=function(){return this.backdrop&&this.backdrop.remove?(this.backdrop.remove(),this.backdrop.overlay=null,this.backdrop.backgroundShown=!1):void 0},e.prototype._showOverlayElement=function(e,o){var n,r,i;return r=t(e.element),n=t(e.backdropElement),!r||0===r.length||this.backdrop.overlayElementShown&&!o?void 0:(this.backdrop.overlayElementShown||(this.backdrop.$element=n.addClass("tour-step-backdrop"),this.backdrop.$background=t("
    ",{"class":"tour-step-background"}),this.backdrop.$background.appendTo(e.backdropContainer),this.backdrop.overlayElementShown=!0),i={width:n.innerWidth(),height:n.innerHeight(),offset:n.offset()},e.backdropPadding&&(i=this._applyBackdropPadding(e.backdropPadding,i)),this.backdrop.$background.width(i.width).height(i.height).offset(i.offset))},e.prototype._hideOverlayElement=function(){return this.backdrop.overlayElementShown?(this.backdrop.$element.removeClass("tour-step-backdrop"),this.backdrop.$background.remove(),this.backdrop.$element=null,this.backdrop.$background=null,this.backdrop.overlayElementShown=!1):void 0},e.prototype._applyBackdropPadding=function(t,e){return"object"==typeof t?(null==t.top&&(t.top=0),null==t.right&&(t.right=0),null==t.bottom&&(t.bottom=0),null==t.left&&(t.left=0),e.offset.top=e.offset.top-t.top,e.offset.left=e.offset.left-t.left,e.width=e.width+t.left+t.right,e.height=e.height+t.top+t.bottom):(e.offset.top=e.offset.top-t,e.offset.left=e.offset.left-t,e.width=e.width+2*t,e.height=e.height+2*t),e},e.prototype._clearTimer=function(){return window.clearTimeout(this._timer),this._timer=null,this._duration=null},e.prototype._getProtocol=function(t){return t=t.split("://"),t.length>1?t[0]:"http"},e.prototype._getHost=function(t){return t=t.split("//"),t=t.length>1?t[1]:t[0],t.split("/")[0]},e.prototype._getPath=function(t){return t.replace(/\/?$/,"").split("?")[0].split("#")[0]},e.prototype._getQuery=function(t){return this._getParams(t,"?")},e.prototype._getHash=function(t){return this._getParams(t,"#")},e.prototype._getParams=function(t,e){var o,n,r,i,s;if(n=t.split(e),1===n.length)return{};for(n=n[1].split("&"),r={},i=0,s=n.length;s>i;i++)o=n[i],o=o.split("="),r[o[0]]=o[1]||"";return r},e.prototype._equal=function(t,e){var o,n,r,i,s,a;if("[object Object]"==={}.toString.call(t)&&"[object Object]"==={}.toString.call(e)){if(n=Object.keys(t),r=Object.keys(e),n.length!==r.length)return!1;for(o in t)if(i=t[o],!this._equal(e[o],i))return!1;return!0}if("[object Array]"==={}.toString.call(t)&&"[object Array]"==={}.toString.call(e)){if(t.length!==e.length)return!1;for(o=s=0,a=t.length;a>s;o=++s)if(i=t[o],!this._equal(i,e[o]))return!1;return!0}return t===e},e}()}); \ No newline at end of file diff --git a/public/lib/intro/intro.min.js b/public/lib/intro/intro.min.js new file mode 100755 index 0000000000..875a703104 --- /dev/null +++ b/public/lib/intro/intro.min.js @@ -0,0 +1,49 @@ +(function(C,n){"object"===typeof exports?n(exports):"function"===typeof define&&define.amd?define(["exports"],n):n(C)})(this,function(C){function n(a){this._targetElement=a;this._introItems=[];this._options={nextLabel:"Next →",prevLabel:"← Back",skipLabel:"Skip",doneLabel:"Done",hidePrev:!1,hideNext:!1,tooltipPosition:"bottom",tooltipClass:"",highlightClass:"",exitOnEsc:!0,exitOnOverlayClick:!0,showStepNumbers:!0,keyboardNavigation:!0,showButtons:!0,showBullets:!0,showProgress:!1,scrollToElement:!0, +overlayOpacity:0.8,scrollPadding:30,positionPrecedence:["bottom","top","right","left"],disableInteraction:!1,hintPosition:"top-middle",hintButtonLabel:"Got it",hintAnimation:!0}}function V(a){var b=[],c=this;if(this._options.steps)for(var d=0,e=this._options.steps.length;de.length)return!1;d=0;for(f=e.length;dk.width||0>h.left+h.width/2-m?(s(g,"bottom"),s(g,"top")):(h.height+h.top+w>k.height&&s(g,"bottom"),0>h.top-w&&s(g,"top"));h.width+h.left+m>k.width&&s(g,"right");0>h.left-m&&s(g,"left");0g.height?(c.className="introjs-arrow left-bottom",b.style.top="-"+(a.height-f.height-20)+"px"):c.className="introjs-arrow left";break;case "left":e||!0!=this._options.showStepNumbers||(b.style.top="15px");f.top+a.height>g.height?(b.style.top="-"+(a.height-f.height-20)+"px", +c.className="introjs-arrow right-bottom"):c.className="introjs-arrow right";b.style.right=f.width+20+"px";break;case "floating":c.style.display="none";b.style.left="50%";b.style.top="50%";b.style.marginLeft="-"+a.width/2+"px";b.style.marginTop="-"+a.height/2+"px";"undefined"!=typeof d&&null!=d&&(d.style.left="-"+(a.width/2+18)+"px",d.style.top="-"+(a.height/2+18)+"px");break;case "bottom-right-aligned":c.className="introjs-arrow top-right";P(f,0,a,b);b.style.top=f.height+20+"px";break;case "bottom-middle-aligned":c.className= +"introjs-arrow top-middle";c=f.width/2-a.width/2;e&&(c+=5);P(f,c,a,b)&&(b.style.right=null,H(f,c,a,g,b));b.style.top=f.height+20+"px";break;default:c.className="introjs-arrow top",H(f,0,a,g,b),b.style.top=f.height+20+"px"}}}function H(a,b,c,d,e){if(a.left+b+c.width>d.width)return e.style.left=d.width-c.width-a.left+"px",!1;e.style.left=b+"px";return!0}function P(a,b,c,d){if(0>a.left+a.width-b-c.width)return d.style.left=-a.left+"px",!1;d.style.right=b+"px";return!0}function s(a,b){-1 a.active").className="",d.querySelector('.introjs-bullets li > a[data-stepnumber="'+a.step+'"]').className="active");d.querySelector(".introjs-progress .introjs-progressbar").setAttribute("style","width:"+Q.call(b)+"%;");w.style.opacity=1;f&&(f.style.opacity=1);-1===l.tabIndex?m.focus():l.focus()},350)}else{var n=document.createElement("div"),h=document.createElement("div"),c=document.createElement("div"),q=document.createElement("div"),r=document.createElement("div"), +s=document.createElement("div"),v=document.createElement("div"),A=document.createElement("div");n.className=e;h.className="introjs-tooltipReferenceLayer";t.call(b,n);t.call(b,h);this._targetElement.appendChild(n);this._targetElement.appendChild(h);c.className="introjs-arrow";r.className="introjs-tooltiptext";r.innerHTML=a.intro;s.className="introjs-bullets";!1===this._options.showBullets&&(s.style.display="none");for(var n=document.createElement("ul"),e=0,C=this._introItems.length;ec||a.element.clientHeight>p?window.scrollBy(0,c-this._options.scrollPadding):window.scrollBy(0,q+70+this._options.scrollPadding));"undefined"!==typeof this._introAfterChangeCallback&&this._introAfterChangeCallback.call(this,a.element)}function O(){for(var a=document.querySelectorAll(".introjs-showElement"), +b=0,c=a.length;bc||"none"!==d&&void 0!==d)b.className+=" introjs-fixParent";b=b.parentNode}}function J(a, +b){if(a instanceof SVGElement){var c=a.getAttribute("class")||"";a.setAttribute("class",c+" "+b)}else a.className+=" "+b}function r(a,b){var c="";a.currentStyle?c=a.currentStyle[b]:document.defaultView&&document.defaultView.getComputedStyle&&(c=document.defaultView.getComputedStyle(a,null).getPropertyValue(b));return c&&c.toLowerCase?c.toLowerCase():c}function I(a){var b=a.parentNode;return b&&"HTML"!==b.nodeName?"fixed"==r(a,"position")?!0:I(b):!1}function G(){if(void 0!=window.innerWidth)return{width:window.innerWidth, +height:window.innerHeight};var a=document.documentElement;return{width:a.clientWidth,height:a.clientHeight}}function Z(a){a=a.getBoundingClientRect();return 0<=a.top&&0<=a.left&&a.bottom+80<=window.innerHeight&&a.right<=window.innerWidth}function W(a){var b=document.createElement("div"),c="",d=this;b.className="introjs-overlay";if(a.tagName&&"body"!==a.tagName.toLowerCase()){var e=u(a);e&&(c+="width: "+e.width+"px; height:"+e.height+"px; top:"+e.top+"px;left: "+e.left+"px;",b.setAttribute("style", +c))}else c+="top: 0;bottom: 0; left: 0;right: 0;position: fixed;",b.setAttribute("style",c);a.appendChild(b);b.onclick=function(){!0==d._options.exitOnOverlayClick&&z.call(d,a)};setTimeout(function(){c+="opacity: "+d._options.overlayOpacity.toString()+";";b.setAttribute("style",c)},10);return!0}function v(){var a=this._targetElement.querySelector(".introjs-hintReference");if(a){var b=a.getAttribute("data-step");a.parentNode.removeChild(a);return b}}function R(a){this._introItems=[];if(this._options.hints){a= +0;for(var b=this._options.hints.length;ac.length)return!1;a=0;for(b=c.length;atd,tr.introjs-showElement>th{z-index:9999999!important}.introjs-disableInteraction{z-index:99999999!important;position:absolute;background-color:white;opacity:0;filter:alpha(opacity=0)}.introjs-relativePosition,tr.introjs-showElement>td,tr.introjs-showElement>th{position:relative}.introjs-helperLayer{box-sizing:content-box;position:absolute;z-index:9999998;background-color:#FFF;background-color:rgba(255,255,255,.9);border:1px solid #777;border:1px solid rgba(0,0,0,.5);border-radius:4px;box-shadow:0 2px 15px rgba(0,0,0,.4);-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-tooltipReferenceLayer{box-sizing:content-box;position:absolute;visibility:hidden;z-index:10000000;background-color:transparent;-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-helperLayer *,.introjs-helperLayer *:before,.introjs-helperLayer *:after{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;-ms-box-sizing:content-box;-o-box-sizing:content-box;box-sizing:content-box}.introjs-helperNumberLayer{box-sizing:content-box;position:absolute;visibility:visible;top:-16px;left:-16px;z-index:9999999999!important;padding:2px;font-family:Arial,verdana,tahoma;font-size:13px;font-weight:bold;color:white;text-align:center;text-shadow:1px 1px 1px rgba(0,0,0,.3);background:#ff3019;background:-webkit-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ff3019),color-stop(100%,#cf0404));background:-moz-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-ms-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-o-linear-gradient(top,#ff3019 0,#cf0404 100%);background:linear-gradient(to bottom,#ff3019 0,#cf0404 100%);width:20px;height:20px;line-height:20px;border:3px solid white;border-radius:50%;filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3019', endColorstr='#cf0404', GradientType=0)";filter:"progid:DXImageTransform.Microsoft.Shadow(direction=135, strength=2, color=ff0000)";box-shadow:0 2px 5px rgba(0,0,0,.4)}.introjs-arrow{border:5px solid white;content:'';position:absolute}.introjs-arrow.top{top:-10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.top-right{top:-10px;right:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.top-middle{top:-10px;left:50%;margin-left:-5px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.right{right:-10px;top:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent;border-left-color:white}.introjs-arrow.right-bottom{bottom:10px;right:-10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent;border-left-color:white}.introjs-arrow.bottom{bottom:-10px;border-top-color:white;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.introjs-arrow.left{left:-10px;top:10px;border-top-color:transparent;border-right-color:white;border-bottom-color:transparent;border-left-color:transparent}.introjs-arrow.left-bottom{left:-10px;bottom:10px;border-top-color:transparent;border-right-color:white;border-bottom-color:transparent;border-left-color:transparent}.introjs-tooltip{box-sizing:content-box;position:absolute;visibility:visible;padding:10px;background-color:white;min-width:200px;max-width:300px;border-radius:3px;box-shadow:0 1px 10px rgba(0,0,0,.4);-webkit-transition:opacity .1s ease-out;-moz-transition:opacity .1s ease-out;-ms-transition:opacity .1s ease-out;-o-transition:opacity .1s ease-out;transition:opacity .1s ease-out}.introjs-tooltipbuttons{text-align:right;white-space:nowrap}.introjs-button{box-sizing:content-box;position:relative;overflow:visible;display:inline-block;padding:.3em .8em;border:1px solid #d4d4d4;margin:0;text-decoration:none;text-shadow:1px 1px 0 #fff;font:11px/normal sans-serif;color:#333;white-space:nowrap;cursor:pointer;outline:0;background-color:#ececec;background-image:-webkit-gradient(linear,0 0,0 100%,from(#f4f4f4),to(#ececec));background-image:-moz-linear-gradient(#f4f4f4,#ececec);background-image:-o-linear-gradient(#f4f4f4,#ececec);background-image:linear-gradient(#f4f4f4,#ececec);-webkit-background-clip:padding;-moz-background-clip:padding;-o-background-clip:padding-box;-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;zoom:1;*display:inline;margin-top:10px}.introjs-button:hover{border-color:#bcbcbc;text-decoration:none;box-shadow:0 1px 1px #e3e3e3}.introjs-button:focus,.introjs-button:active{background-image:-webkit-gradient(linear,0 0,0 100%,from(#ececec),to(#f4f4f4));background-image:-moz-linear-gradient(#ececec,#f4f4f4);background-image:-o-linear-gradient(#ececec,#f4f4f4);background-image:linear-gradient(#ececec,#f4f4f4)}.introjs-button::-moz-focus-inner{padding:0;border:0}.introjs-skipbutton{box-sizing:content-box;margin-right:5px;color:#7a7a7a}.introjs-prevbutton{-webkit-border-radius:.2em 0 0 .2em;-moz-border-radius:.2em 0 0 .2em;border-radius:.2em 0 0 .2em;border-right:0}.introjs-prevbutton.introjs-fullbutton{border:1px solid #d4d4d4;-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em}.introjs-nextbutton{-webkit-border-radius:0 .2em .2em 0;-moz-border-radius:0 .2em .2em 0;border-radius:0 .2em .2em 0}.introjs-nextbutton.introjs-fullbutton{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em}.introjs-disabled,.introjs-disabled:hover,.introjs-disabled:focus{color:#9a9a9a;border-color:#d4d4d4;box-shadow:none;cursor:default;background-color:#f4f4f4;background-image:none;text-decoration:none}.introjs-hidden{display:none}.introjs-bullets{text-align:center}.introjs-bullets ul{box-sizing:content-box;clear:both;margin:15px auto 0;padding:0;display:inline-block}.introjs-bullets ul li{box-sizing:content-box;list-style:none;float:left;margin:0 2px}.introjs-bullets ul li a{box-sizing:content-box;display:block;width:6px;height:6px;background:#ccc;border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px;text-decoration:none;cursor:pointer}.introjs-bullets ul li a:hover{background:#999}.introjs-bullets ul li a.active{background:#999}.introjs-progress{box-sizing:content-box;overflow:hidden;height:10px;margin:10px 0 5px 0;border-radius:4px;background-color:#ecf0f1}.introjs-progressbar{box-sizing:content-box;float:left;width:0;height:100%;font-size:10px;line-height:10px;text-align:center;background-color:#08c}.introjsFloatingElement{position:absolute;height:0;width:0;left:50%;top:50%}.introjs-fixedTooltip{position:fixed}.introjs-hint{box-sizing:content-box;position:absolute;background:transparent;width:20px;height:15px;cursor:pointer}.introjs-hint:focus{border:0;outline:0}.introjs-hidehint{display:none}.introjs-fixedhint{position:fixed}.introjs-hint:hover>.introjs-hint-pulse{border:5px solid rgba(60,60,60,0.57)}.introjs-hint-pulse{box-sizing:content-box;width:10px;height:10px;border:5px solid rgba(60,60,60,0.27);-webkit-border-radius:30px;-moz-border-radius:30px;border-radius:30px;background-color:rgba(136,136,136,0.24);z-index:10;position:absolute;-webkit-transition:all .2s ease-out;-moz-transition:all .2s ease-out;-ms-transition:all .2s ease-out;-o-transition:all .2s ease-out;transition:all .2s ease-out}.introjs-hint-no-anim .introjs-hint-dot{-webkit-animation:none;-moz-animation:none;animation:none}.introjs-hint-dot{box-sizing:content-box;border:10px solid rgba(146,146,146,0.36);background:transparent;-webkit-border-radius:60px;-moz-border-radius:60px;border-radius:60px;height:50px;width:50px;-webkit-animation:introjspulse 3s ease-out;-moz-animation:introjspulse 3s ease-out;animation:introjspulse 3s ease-out;-webkit-animation-iteration-count:infinite;-moz-animation-iteration-count:infinite;animation-iteration-count:infinite;position:absolute;top:-25px;left:-25px;z-index:1;opacity:0}@-moz-keyframes introjspulse{0%{-moz-transform:scale(0);opacity:.0}25%{-moz-transform:scale(0);opacity:.1}50%{-moz-transform:scale(0.1);opacity:.3}75%{-moz-transform:scale(0.5);opacity:.5}100%{-moz-transform:scale(1);opacity:.0}}@-webkit-keyframes introjspulse{0%{-webkit-transform:scale(0);opacity:.0}25%{-webkit-transform:scale(0);opacity:.1}50%{-webkit-transform:scale(0.1);opacity:.3}75%{-webkit-transform:scale(0.5);opacity:.5}100%{-webkit-transform:scale(1);opacity:.0}} \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index 0b22ea1677..e99614ce90 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -17,5 +17,4 @@ "display": "standalone", "start_url": "/", "orientation": "portrait" - } diff --git a/public/safari-pinned-tab.svg b/public/safari-pinned-tab.svg index cfa922579b..b433d7b8ed 100644 --- a/public/safari-pinned-tab.svg +++ b/public/safari-pinned-tab.svg @@ -1,15 +1,15 @@ + "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - + + Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + - - - - + diff --git a/resources/lang/en_US/breadcrumbs.php b/resources/lang/en_US/breadcrumbs.php index 54cb260179..28c31b9787 100644 --- a/resources/lang/en_US/breadcrumbs.php +++ b/resources/lang/en_US/breadcrumbs.php @@ -24,7 +24,7 @@ return [ 'edit_bill' => 'Edit bill ":name"', 'delete_bill' => 'Delete bill ":name"', 'reports' => 'Reports', - 'searchResult' => 'Search for ":query"', + 'search_result' => 'Search results for ":query"', 'withdrawal_list' => 'Expenses', 'deposit_list' => 'Revenue, income and deposits', 'transfer_list' => 'Transfers', diff --git a/resources/lang/en_US/csv.php b/resources/lang/en_US/csv.php index d5306e2a88..f262f83b10 100644 --- a/resources/lang/en_US/csv.php +++ b/resources/lang/en_US/csv.php @@ -13,36 +13,37 @@ declare(strict_types=1); return [ - 'import_configure_title' => 'Configure your import', - 'import_configure_intro' => 'There are some options for your CSV import. Please indicate if your CSV file contains headers on the first column, and what the date format of your date-fields is. That might require some experimentation. The field delimiter is usually a ",", but could also be a ";". Check this carefully.', - 'import_configure_form' => 'Basic CSV import options', - 'header_help' => 'Check this if the first row of your CSV file are the column titles', - 'date_help' => 'Date time format in your CSV. Follow the format like this page indicates. The default value will parse dates that look like this: :dateExample.', - 'delimiter_help' => 'Choose the field delimiter that is used in your input file. If not sure, comma is the safest option.', - 'import_account_help' => 'If your CSV file does NOT contain information about your asset account(s), use this dropdown to select to which account the transactions in the CSV belong to.', - 'upload_not_writeable' => 'The grey box contains a file path. It should be writeable. Please make sure it is.', + // initial config + 'initial_title' => 'Import setup (1/3) - Basic CSV import setup', + 'initial_text' => 'To be able to import your file correctly, please validate the options below.', + 'initial_box' => 'Basic CSV import setup', + 'initial_header_help' => 'Check this box if the first row of your CSV file are the column titles.', + 'initial_date_help' => 'Date time format in your CSV. Follow the format like this page indicates. The default value will parse dates that look like this: :dateExample.', + 'initial_delimiter_help' => 'Choose the field delimiter that is used in your input file. If not sure, comma is the safest option.', + 'initial_import_account_help' => 'If your CSV file does NOT contain information about your asset account(s), use this dropdown to select to which account the transactions in the CSV belong to.', + 'initial_submit' => 'Continue with step 2/3', - // roles - 'column_roles_title' => 'Define column roles', - 'column_roles_table' => 'Table', - 'column_name' => 'Name of column', - 'column_example' => 'Column example data', - 'column_role' => 'Column data meaning', - 'do_map_value' => 'Map these values', - 'column' => 'Column', - 'no_example_data' => 'No example data available', - 'store_column_roles' => 'Continue import', - 'do_not_map' => '(do not map)', - 'map_title' => 'Connect import data to Firefly III data', - 'map_text' => 'In the following tables, the left value shows you information found in your uploaded CSV file. It is your task to map this value, if possible, to a value already present in your database. Firefly will stick to this mapping. If there is no value to map to, or you do not wish to map the specific value, select nothing.', + // roles config + 'roles_title' => 'Import setup (2/3) - Define each column\'s role', + 'roles_text' => 'Each column in your CSV file contains certain data. Please indicate what kind of data the importer should expect. The option to "map" data means that you will link each entry found in the column to a value in your database. An often mapped column is the column that contains the IBAN of the opposing account. That can be easily matched to IBAN\'s present in your database already.', + 'roles_table' => 'Table', + 'roles_column_name' => 'Name of column', + 'roles_column_example' => 'Column example data', + 'roles_column_role' => 'Column data meaning', + 'roles_do_map_value' => 'Map these values', + 'roles_column' => 'Column', + 'roles_no_example_data' => 'No example data available', + 'roles_submit' => 'Continue with step 3/3', - 'field_value' => 'Field value', - 'field_mapped_to' => 'Mapped to', - 'store_column_mapping' => 'Store mapping', + // map data + 'map_title' => 'Import setup (3/3) - Connect import data to Firefly III data', + 'map_text' => 'In the following tables, the left value shows you information found in your uploaded CSV file. It is your task to map this value, if possible, to a value already present in your database. Firefly will stick to this mapping. If there is no value to map to, or you do not wish to map the specific value, select nothing.', + 'map_field_value' => 'Field value', + 'map_field_mapped_to' => 'Mapped to', + 'map_do_not_map' => '(do not map)', + 'map_submit' => 'Start the import', // map things. - - 'column__ignore' => '(ignore this column)', 'column_account-iban' => 'Asset account (IBAN)', 'column_account-id' => 'Asset account ID (matching Firefly)', diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index c1523710c5..95b3bec834 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -20,14 +20,14 @@ return [ 'everything' => 'Everything', 'customRange' => 'Custom range', 'apply' => 'Apply', + 'select_date' => 'Select date..', 'cancel' => 'Cancel', 'from' => 'From', 'to' => 'To', 'showEverything' => 'Show everything', 'never' => 'Never', 'search_results_for' => 'Search results for ":query"', - 'advanced_search' => 'Advanced search', - 'advanced_search_intro' => 'There are several modifiers that you can use in your search to narrow down the results. If you use any of these, the search will only return transactions. Please click the -icon for more information.', + 'no_results_for_empty_search' => 'Your search was empty, so nothing was found.', 'bounced_error' => 'The message sent to :email bounced, so no access for you.', 'deleted_error' => 'These credentials do not match our records.', 'general_blocked_error' => 'Your account has been disabled, so you cannot login.', @@ -68,13 +68,6 @@ return [ 'two_factor_lost_fix_owner' => 'Otherwise, email the site owner, :site_owner and ask them to reset your two factor authentication.', 'warning_much_data' => ':days days of data may take a while to load.', 'registered' => 'You have registered successfully!', - 'search' => 'Search', - 'search_found_accounts' => 'Found :count account(s) for your query.', - 'search_found_categories' => 'Found :count category(ies) for your query.', - 'search_found_budgets' => 'Found :count budget(s) for your query.', - 'search_found_tags' => 'Found :count tag(s) for your query.', - 'search_found_transactions' => 'Found :count transaction(s) for your query.', - 'results_limited' => 'The results are limited to :count entries.', 'tagbalancingAct' => 'Balancing act', 'tagadvancePayment' => 'Advance payment', 'tagnothing' => '', @@ -112,8 +105,8 @@ return [ 'budget_in_period' => 'All transactions for budget ":name" between :start and :end', 'chart_budget_in_period' => 'Chart for all transactions for budget ":name" between :start and :end', 'chart_account_in_period' => 'Chart for all transactions for account ":name" between :start and :end', - 'chart_category_in_period' => 'Chart for all transactions for category ":name" between :start and :end', - 'chart_category_all' => 'Chart for all transactions for category ":name"', + 'chart_category_in_period' => 'Chart for all transactions for category ":name" between :start and :end', + 'chart_category_all' => 'Chart for all transactions for category ":name"', 'budget_in_period_breadcrumb' => 'Between :start and :end', 'clone_withdrawal' => 'Clone this withdrawal', 'clone_deposit' => 'Clone this deposit', @@ -149,6 +142,16 @@ return [ 'not_available_demo_user' => 'The feature you try to access is not available to demo users.', 'exchange_rate_instructions' => 'Asset account "@name" only accepts transactions in @native_currency. If you wish to use @foreign_currency instead, make sure that the amount in @native_currency is known as well:', 'transfer_exchange_rate_instructions' => 'Source asset account "@source_name" only accepts transactions in @source_currency. Destination asset account "@dest_name" only accepts transactions in @dest_currency. You must provide the transferred amount correctly in both currencies.', + 'transaction_data' => 'Transaction data', + + // search + 'search' => 'Search', + 'search_found_transactions' => 'Number of transactions found:', + 'general_search_error' => 'An error occured while searching. Please check the log files for more information.', + 'search_box' => 'Search', + 'search_box_intro' => 'Welcome to the search function of Firefly III. Enter your search query in the box. Make sure you check out the help file because the search is pretty advanced.', + 'search_error' => 'Error while searching', + 'search_searching' => 'Searching ...', // repeat frequencies: 'repeat_freq_yearly' => 'yearly', @@ -424,12 +427,6 @@ return [ 'attachment_updated' => 'Updated attachment ":name"', 'upload_max_file_size' => 'Maximum file size: :size', - // tour: - 'prev' => 'Prev', - 'next' => 'Next', - 'end-tour' => 'End tour', - 'pause' => 'Pause', - // transaction index 'title_expenses' => 'Expenses', 'title_withdrawal' => 'Expenses', @@ -497,15 +494,6 @@ return [ 'make_default_currency' => 'make default', 'default_currency' => 'default', - // new user: - 'submit' => 'Submit', - 'getting_started' => 'Getting started', - 'to_get_started' => 'To get started with Firefly, please enter your current bank\'s name, and the balance of your checking account:', - 'savings_balance_text' => 'If you have a savings account, please enter the current balance of your savings account:', - 'cc_balance_text' => 'If you have a credit card, please enter your credit card\'s limit.', - 'stored_new_account_new_user' => 'Yay! Your new account has been stored.', - 'stored_new_accounts_new_user' => 'Yay! Your new accounts have been stored.', - // forms: 'mandatoryFields' => 'Mandatory fields', 'optionalFields' => 'Optional fields', @@ -641,6 +629,12 @@ return [ // new user: 'welcome' => 'Welcome to Firefly!', + 'submit' => 'Submit', + 'getting_started' => 'Getting started', + 'to_get_started' => 'It is good to see you have successfully installed Firefly III. To get started with this tool please enter your bank\'s name and the balance of your main checking account. Do not worry yet if you have multiple accounts. You can add those later. It\'s just that Firefly III needs something to start with.', + 'savings_balance_text' => 'Firefly III will automatically create a savings account for you. By default, there will be no money in your savings account, but if you tell Firefly III the balance it will be stored as such.', + 'finish_up_new_user' => 'That\'s it! You can continue by pressing Submit. You will be taken to the index of Firefly III.', + 'stored_new_accounts_new_user' => 'Yay! Your new accounts have been stored.', // home page: 'yourAccounts' => 'Your accounts', @@ -962,52 +956,54 @@ return [ 'split_this_transfer' => 'Split this transfer', 'cannot_edit_multiple_source' => 'You cannot edit splitted transaction #:id with description ":description" because it contains multiple source accounts.', 'cannot_edit_multiple_dest' => 'You cannot edit splitted transaction #:id with description ":description" because it contains multiple destination accounts.', + 'cannot_edit_opening_balance' => 'You cannot edit the opening balance of an account.', 'no_edit_multiple_left' => 'You have selected no valid transactions to edit.', - // import - 'configuration_file_help' => 'If you have previously imported data into Firefly III, you may have a configuration file, which will pre-set configuration values for you. For some banks, other users have kindly provided their configuration file.', - 'import_data_index' => 'Index', - 'import_file_type_csv' => 'CSV (comma separated values)', - 'import_file_type_help' => 'Select the type of file you will upload', - 'import_start' => 'Start the import', - 'configure_import' => 'Further configure your import', - 'import_finish_configuration' => 'Finish configuration', - 'settings_for_import' => 'Settings', - 'import_status' => 'Import status', - 'import_status_text' => 'The import is currently running, or will start momentarily.', - 'import_complete' => 'Import configuration complete!', - 'import_complete_text' => 'The import is ready to start. All the configuration you needed to do has been done. Please download the configuration file. It will help you with the import should it not go as planned. To actually run the import, you can either execute the following command in your console, or run the web-based import. Depending on your configuration, the console import will give you more feedback.', - 'import_download_config' => 'Download configuration', - 'import_start_import' => 'Start import', - 'import_data' => 'Import data', - 'import_data_full' => 'Import data into Firefly III', + // import bread crumbs and titles: 'import' => 'Import', - 'import_file_help' => 'Select your file', - 'import_status_settings_complete' => 'The import is ready to start.', - 'import_status_import_complete' => 'The import has completed.', - 'import_status_import_running' => 'The import is currently running. Please be patient.', - 'import_status_header' => 'Import status and progress', - 'import_status_errors' => 'Import errors', - 'import_status_report' => 'Import report', - 'import_finished' => 'Import has finished', - 'import_error_single' => 'An error has occured during the import.', - 'import_error_multi' => 'Some errors occured during the import.', - 'import_error_fatal' => 'There was an error during the import routine. Please check the log files. The error seems to be:', - 'import_error_timeout' => 'The import seems to have timed out. If this error persists, please import your data using the console command.', - 'import_double' => 'Row #:row: This row has been imported before, and is stored in :description.', - 'import_finished_all' => 'The import has finished. Please check out the results below.', + 'import_data' => 'Import data', + + // import index page: + 'import_index_title' => 'Import data into Firefly III', + 'import_index_sub_title' => 'Index', + 'import_index_intro' => 'Welcome to Firefly\'s import routine. These pages can help you import data from your bank into Firefly III. Please check out the help pages in the top right corner.', + 'import_index_file' => 'Select your file', + 'import_index_config' => 'If you have previously imported data into Firefly III, you may have a configuration file, which will pre-set configuration values for you. For some banks, other users have kindly provided their configuration file.', + 'import_index_type' => 'Select the type of file you will upload', + 'import_index_start' => 'Start importing', + + // supported file types: + 'import_file_type_csv' => 'CSV (comma separated values)', + + // import configuration routine: + 'import_config_sub_title' => 'Set up your import file', + 'import_config_bread_crumb' => 'Set up your import file', + + // import status page: + 'import_status_bread_crumb' => 'Import status', + 'import_status_sub_title' => 'Import status', + 'import_status_wait_title' => 'Please hold...', + 'import_status_wait_text' => 'This box will disappear in a moment.', + 'import_status_ready_title' => 'Import is ready to start', + 'import_status_ready_text' => 'The import is ready to start. All the configuration you needed to do has been done. Please download the configuration file. It will help you with the import should it not go as planned. To actually run the import, you can either execute the following command in your console, or run the web-based import. Depending on your configuration, the console import will give you more feedback.', + 'import_status_ready_config' => 'Download configuration', + 'import_status_ready_start' => 'Start the import', + 'import_status_ready_share' => 'Please consider downloading your configuration and sharing it at the import configuration center. This will allow other users of Firefly III to import their files more easily.', + 'import_status_running_title' => 'The import is running', + 'import_status_running_placeholder' => 'Please hold for an update...', + 'import_status_errors_title' => 'Errors during the import', + 'import_status_errors_single' => 'An error has occured during the import. It does not appear to be fatal.', + 'import_status_errors_multi' => 'Some errors occured during the import. These do not appear to be fatal.', + 'import_status_fatal_title' => 'A fatal error occurred', + 'import_status_fatal_text' => 'A fatal error occurred, which the import-routine cannot recover from. Please see the explanation in red below.', + 'import_status_fatal_more' => 'If the error is a time-out, the import will have stopped half-way. For some server configurations, it is merely the server that stopped while the import keeps running in the background. To verify this, check out the log files. If the problem persists, consider importing over the command line instead.', + 'import_status_finished_title' => 'Import routine finished', + 'import_status_finished_text' => 'The import routine has imported your file.', + 'import_status_finished_job' => 'The transactions imported can be found in tag :tag.', 'import_with_key' => 'Import with key \':key\'', - 'import_share_configuration' => 'Please consider downloading your configuration and sharing it at the import configuration center. This will allow other users of Firefly III to import their files more easily.', - 'import_finished_report' => 'The import has finished. Please note any errors in the block above this line. All transactions imported during this particular session have been tagged, and you can check them out below. ', - 'import_finished_link' => 'The transactions imported can be found in tag :tag.', - 'need_at_least_one_account' => 'You need at least one asset account to be able to create piggy banks', - 'see_help_top_right' => 'For more information, please check out the help pages using the icon in the top right corner of the page.', - 'bread_crumb_import_complete' => 'Import ":key" complete', - 'bread_crumb_configure_import' => 'Configure import ":key"', - 'bread_crumb_import_finished' => 'Import ":key" finished', - 'import_finished_intro' => 'The import has finished! You can now see the new transactions in Firefly.', - 'import_finished_text_without_link' => 'It seems there is no tag that points to all your imported transactions. Please look for your imported data in the menu on the left, under "Transactions".', - 'import_finished_text_with_link' => 'You can find a list of your imported transactions on the page of the tag that was created for this import.', + + // different states: + 'import_status_job_running' => 'The import is underway. Please be patient...', // sandstorm.io errors and messages: 'sandstorm_not_available' => 'This function is not available when you are using Firefly III within a Sandstorm.io environment.', diff --git a/resources/views/accounts/edit.twig b/resources/views/accounts/edit.twig index 5123ee9751..e9ce4d5516 100644 --- a/resources/views/accounts/edit.twig +++ b/resources/views/accounts/edit.twig @@ -10,7 +10,7 @@
    -
    +

    {{ 'mandatoryFields'|_ }}

    @@ -25,7 +25,7 @@
    -
    +

    {{ 'optionalFields'|_ }}

    diff --git a/resources/views/accounts/show.twig b/resources/views/accounts/show.twig index bedb9ee721..7249618ff8 100644 --- a/resources/views/accounts/show.twig +++ b/resources/views/accounts/show.twig @@ -6,7 +6,7 @@ {% block content %}
    -
    +

    @@ -16,7 +16,6 @@ {{ trans('firefly.chart_account_in_period', {name: account.name, start: start.formatLocalized(monthAndDayFormat), end: end.formatLocalized(monthAndDayFormat) }) }} {% endif %}

    -
    @@ -73,20 +72,20 @@
    {% if periods.count > 0 %}
    - {% endif %}
    -
    +

    {{ 'transactions'|_ }}

    - {% include 'list.journals-tasker' with {sorting:true, hideBills:true, hideBudgets: true, hideCategories: true} %} + {% include 'list.journals' with {sorting:true, hideBills:true, hideBudgets: true, hideCategories: true} %} {% if periods.count > 0 %}

    @@ -106,7 +105,7 @@

    {% if periods.count > 0 %} -
    +
    {% for period in periods %} {% if (period.spent != 0 or period.earned != 0) %}
    @@ -119,13 +118,13 @@ {% if period.spent != 0 %} {{ 'spent'|_ }} - {{ period.spent|formatAmount }} + {{ formatAmountByCurrency(currency, period.spent) }} {% endif %} {% if period.earned != 0 %} {{ 'earned'|_ }} - {{ period.earned|formatAmount }} + {{ formatAmountByCurrency(currency, period.earned) }} {% endif %} diff --git a/resources/views/bills/create.twig b/resources/views/bills/create.twig index 071771ab32..c182cb2976 100644 --- a/resources/views/bills/create.twig +++ b/resources/views/bills/create.twig @@ -10,7 +10,7 @@
    -
    +

    {{ 'mandatoryFields'|_ }}

    @@ -26,7 +26,7 @@
    -
    +

    {{ 'optionalFields'|_ }}

    diff --git a/resources/views/bills/edit.twig b/resources/views/bills/edit.twig index dcda9c47e0..ded9774dc8 100644 --- a/resources/views/bills/edit.twig +++ b/resources/views/bills/edit.twig @@ -11,7 +11,7 @@
    -
    +

    {{ 'mandatoryFields'|_ }}

    @@ -28,7 +28,7 @@
    -
    +

    {{ 'optionalFields'|_ }}

    diff --git a/resources/views/bills/index.twig b/resources/views/bills/index.twig index b56f32da2b..55d2280420 100644 --- a/resources/views/bills/index.twig +++ b/resources/views/bills/index.twig @@ -31,4 +31,4 @@
    {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/resources/views/bills/show.twig b/resources/views/bills/show.twig index 2b01b46c29..7e81359214 100644 --- a/resources/views/bills/show.twig +++ b/resources/views/bills/show.twig @@ -106,7 +106,7 @@

    {{ 'connected_journals'|_ }}

    - {% include 'list.journals-tasker' %} + {% include 'list.journals' %}
    diff --git a/resources/views/budgets/create.twig b/resources/views/budgets/create.twig index 3974791a23..25e6f9cd10 100644 --- a/resources/views/budgets/create.twig +++ b/resources/views/budgets/create.twig @@ -11,7 +11,7 @@
    -
    +

    {{ 'mandatoryFields'|_ }}

    @@ -21,7 +21,7 @@
    -
    +
    diff --git a/resources/views/budgets/edit.twig b/resources/views/budgets/edit.twig index e8010bc424..977dfad47c 100644 --- a/resources/views/budgets/edit.twig +++ b/resources/views/budgets/edit.twig @@ -8,7 +8,7 @@ {{ Form.model(budget, {'class' : 'form-horizontal','id' : 'update','url' : route('budgets.update',budget.id) } ) }}
    -
    +

    {{ 'mandatoryFields'|_ }}

    @@ -20,7 +20,7 @@
    -
    +
    diff --git a/resources/views/budgets/index.twig b/resources/views/budgets/index.twig index 9ecec09211..1753737b57 100644 --- a/resources/views/budgets/index.twig +++ b/resources/views/budgets/index.twig @@ -6,17 +6,17 @@ {% block content %}
    -
    +

    {{ periodStart }} — {{ periodEnd }}

    -
    +
    {{ 'budgeted'|_ }}: {{ budgeted|formatAmountPlain }}
    -
    +
    {{ trans('firefly.available_between',{start : periodStart, end: periodEnd }) }}: {{ available|formatAmountPlain }} @@ -25,7 +25,7 @@
    -
    +
    @@ -37,12 +37,12 @@
    -
    +
    {{ trans('firefly.spent_between', {start: periodStart, end: periodEnd}) }}: {{ spent|formatAmount }}
    -
    +
    @@ -57,8 +57,7 @@
    -
    - +

    {{ 'transactionsWithoutBudget'|_ }}

    @@ -81,102 +80,116 @@
    {% endif %} -
    {% if budgets.count == 0 and inactive.count == 0 %} {% include 'partials.empty' with {what: 'default', type: 'budgets',route: route('budgets.create')} %} {% endif %} + + {# date thing #}
    - {% for budget in budgets %} -
    +
    -

    - - {% if budgetInformation[budget.id]['currentLimit'] %} - {{ budget.name }} - {% else %} - {{ budget.name }} - {% endif %} -

    - - -
    -
    - - +

    Period thing

    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    -
    - + + + + +
    +
    +
    +
    +

    Budget stuff

    +
    +
    +
    + - - + + + + + + + + + {% for budget in budgets %} + + - - - - + + - {% if budgetInformation[budget.id]['otherLimits'].count > 0 %} - - - - {% endif %} + {% endfor %} +
    - {{ 'budgeted'|_ }} -
    - {{ session('start').formatLocalized(monthAndDayFormat) }} - - {{ session('end').formatLocalized(monthAndDayFormat) }}
    -
    -
    -
    -
    {{ defaultCurrency.symbol|raw }}
    - - {% if budgetInformation[budget.id]['currentLimit'] %} - {% set repAmount = budgetInformation[budget.id]['currentLimit'].amount %} - {% else %} - {% set repAmount = '0' %} - {% endif %} - -
    +
    {{ 'budget'|_ }}{{ 'budgeted'|_ }}
    - {{ 'spent'|_ }} -
    - {{ session('start').formatLocalized(monthAndDayFormat) }} - - {{ session('end').formatLocalized(monthAndDayFormat) }} -
    +
    + {% if budgetInformation[budget.id]['currentLimit'] %} + {{ budget.name }} + {% else %} + {{ budget.name }} + {% endif %} + {% if budgetInformation[budget.id]['currentLimit'] %} + {% set repAmount = budgetInformation[budget.id]['currentLimit'].amount %} + {% else %} + {% set repAmount = '0' %} + {% endif %} + + +
    +
    {{ defaultCurrency.symbol|raw }}
    + + +
    +
    -
      - {% for other in budgetInformation[budget.id]['otherLimits'] %} -
    • - - Budgeted - {{ other.amount|formatAmountPlain }} - between - {{ other.start_date.formatLocalized(monthAndDayFormat) }} - and {{ other.end_date.formatLocalized(monthAndDayFormat) }}. -
    • - {% endfor %} -
    -
    - {% if loop.index % 3 == 0 %} -
    - {% endif %} - {% endfor %}
    {% if inactive|length > 0 %} @@ -200,6 +213,11 @@
    {% endif %} {% endblock %} + +{% block styles %} + +{% endblock %} + {% block scripts %} - + {% endblock %} diff --git a/resources/views/budgets/no-budget.twig b/resources/views/budgets/no-budget.twig index fd23bde560..e400c32a98 100644 --- a/resources/views/budgets/no-budget.twig +++ b/resources/views/budgets/no-budget.twig @@ -9,20 +9,20 @@ {# upper show-all instruction #} {% if periods.count > 0 %}
    - {% endif %}
    -
    +

    {{ subTitle }}

    - {% include 'list.journals-tasker' with {'journals': journals,'hideBudgets': true} %} + {% include 'list.journals' with {'journals': journals,'hideBudgets': true} %} {% if periods.count > 0 %}

    @@ -39,7 +39,7 @@

    {% if periods.count > 0 %} -
    +
    {% for period in periods %} {% if period.count > 0 %}
    @@ -70,7 +70,7 @@ {# lower show-all instruction #} {% if periods.count > 0 %}
    - diff --git a/resources/views/budgets/show.twig b/resources/views/budgets/show.twig index 3e598b1d6e..824efb1ce2 100644 --- a/resources/views/budgets/show.twig +++ b/resources/views/budgets/show.twig @@ -88,7 +88,7 @@

    {{ 'transactions'|_ }}

    - {% include 'list.journals-tasker' with {hideBudgets:true, hideBills:true} %} + {% include 'list.journals' with {hideBudgets:true, hideBills:true} %} {% if budgetLimit %}

    diff --git a/resources/views/categories/create.twig b/resources/views/categories/create.twig index e99d27b20a..364e40ca1f 100644 --- a/resources/views/categories/create.twig +++ b/resources/views/categories/create.twig @@ -9,7 +9,7 @@

    -
    +

    {{ 'mandatoryFields'|_ }}

    @@ -20,7 +20,7 @@
    -
    +
    diff --git a/resources/views/categories/index.twig b/resources/views/categories/index.twig index 8316a444b9..14547ec802 100644 --- a/resources/views/categories/index.twig +++ b/resources/views/categories/index.twig @@ -33,9 +33,12 @@ {% include 'partials.empty' with {what: 'default', type: 'categories',route: route('categories.create')} %} {% endif %} {% endblock %} + {% block styles %} + {% endblock %} {% block scripts %} + {% endblock %} diff --git a/resources/views/categories/no-category.twig b/resources/views/categories/no-category.twig index c6ebc131f4..64a8dfc793 100644 --- a/resources/views/categories/no-category.twig +++ b/resources/views/categories/no-category.twig @@ -9,20 +9,20 @@ {# upper show-all instruction #} {% if periods.count > 0 %}
    - {% endif %}
    -
    +

    {{ subTitle }}

    - {% include 'list.journals-tasker' with {'journals': journals, 'hideCategories':true} %} + {% include 'list.journals' with {'journals': journals, 'hideCategories':true} %} {% if periods.count > 0 %}

    @@ -39,7 +39,7 @@

    {% if periods.count > 0 %} -
    +
    {% for period in periods %} {% if period.count > 0 %}
    @@ -84,7 +84,7 @@ {# lower show-all instruction #} {% if periods.count > 0 %}
    - diff --git a/resources/views/categories/show.twig b/resources/views/categories/show.twig index 186ad996de..2bb556734e 100644 --- a/resources/views/categories/show.twig +++ b/resources/views/categories/show.twig @@ -8,7 +8,7 @@
    {% if moment != 'all' %} {# both charts #} -
    +

    @@ -20,7 +20,7 @@

    -
    +

    @@ -65,7 +65,7 @@

    {{ 'transactions'|_ }}

    - {% include 'list.journals-tasker' with {hideCategories: true} %} + {% include 'list.journals' with {hideCategories: true, hideBills: true} %} {% if periods.count > 0 %}

    diff --git a/resources/views/currencies/create.twig b/resources/views/currencies/create.twig index 18854f366c..ac98994a17 100644 --- a/resources/views/currencies/create.twig +++ b/resources/views/currencies/create.twig @@ -8,7 +8,7 @@

    -
    +

    {{ 'mandatoryFields'|_ }}

    @@ -23,7 +23,7 @@
    -
    +
    diff --git a/resources/views/currencies/edit.twig b/resources/views/currencies/edit.twig index 1c614ebbb2..68462352da 100644 --- a/resources/views/currencies/edit.twig +++ b/resources/views/currencies/edit.twig @@ -9,7 +9,7 @@
    -
    +

    {{ 'mandatoryFields'|_ }}

    @@ -23,7 +23,7 @@
    -
    +
    diff --git a/resources/views/emails/footer-html.twig b/resources/views/emails/footer-html.twig index 6f4f219380..6e2ca37007 100644 --- a/resources/views/emails/footer-html.twig +++ b/resources/views/emails/footer-html.twig @@ -6,7 +6,7 @@

    - PS: This message was sent because a request from IP {{ ip }} triggered it. + PS: This message was sent because a request from IP {{ ip }}{{ userIp }}{{ ipAddress }} triggered it.

    diff --git a/resources/views/emails/footer-text.twig b/resources/views/emails/footer-text.twig index f3ed37bbbd..e2ec3ff9d4 100644 --- a/resources/views/emails/footer-text.twig +++ b/resources/views/emails/footer-text.twig @@ -3,4 +3,4 @@ Beep boop, The Firefly III Mail Robot -PS: This message was sent because a request from IP {{ ip }} triggered it. +PS: This message was sent because a request from IP {{ ip }}{{ userIp }}{{ ipAddress }} triggered it. diff --git a/resources/views/import/complete.twig b/resources/views/import/complete.twig deleted file mode 100644 index 932dbe2caa..0000000000 --- a/resources/views/import/complete.twig +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "./layout/default" %} - -{% block breadcrumbs %} - {{ Breadcrumbs.renderIfExists }} -{% endblock %} -{% block content %} -
    -
    -
    -
    -

    {{ 'import_complete'|_ }}

    -
    -
    -

    - {{ 'import_complete_text'|_ }} -

    -

    - php artisan firefly:start-import {{ job.key }} -

    - -

    -   -

    -

    - {{ 'import_share_configuration'|_ }} -

    -
    -
    -
    -
    -{% endblock %} -{% block scripts %} -{% endblock %} -{% block styles %} -{% endblock %} diff --git a/resources/views/import/csv/configure.twig b/resources/views/import/csv/initial.twig similarity index 61% rename from resources/views/import/csv/configure.twig rename to resources/views/import/csv/initial.twig index 9a9e53a0b0..bbfbc52d95 100644 --- a/resources/views/import/csv/configure.twig +++ b/resources/views/import/csv/initial.twig @@ -10,11 +10,11 @@
    -

    {{ trans('csv.import_configure_title') }}

    +

    {{ trans('csv.initial_title') }}

    - {{ trans('csv.import_configure_intro') }} + {{ trans('csv.initial_text') }}

    @@ -29,16 +29,16 @@
    -

    {{ trans('csv.import_configure_form') }}

    +

    {{ trans('csv.initial_box_title') }}

    - {{ ExpandedForm.checkbox('has_headers',1,job.configuration['has-headers'],{helpText: trans('csv.header_help')}) }} - {{ ExpandedForm.text('date_format',job.configuration['date-format'],{helpText: trans('csv.date_help', {dateExample: phpdate('Ymd')}) }) }} - {{ ExpandedForm.select('csv_delimiter', data.delimiters, job.configuration['delimiter'], {helpText: trans('csv.delimiter_help') } ) }} - {{ ExpandedForm.select('csv_import_account', data.accounts, job.configuration['import-account'], {helpText: trans('csv.import_account_help')} ) }} + {{ ExpandedForm.checkbox('has_headers',1,job.configuration['has-headers'],{helpText: trans('csv.initial_header_help')}) }} + {{ ExpandedForm.text('date_format',job.configuration['date-format'],{helpText: trans('csv.initial_date_help', {dateExample: phpdate('Ymd')}) }) }} + {{ ExpandedForm.select('csv_delimiter', data.delimiters, job.configuration['delimiter'], {helpText: trans('csv.initial_delimiter_help') } ) }} + {{ ExpandedForm.select('csv_import_account', data.accounts, job.configuration['import-account'], {helpText: trans('csv.initial_import_account_help')} ) }} {% for type, specific in data.specifics %}
    @@ -56,40 +56,23 @@
    {% endfor %} - - {% if not data.is_upload_possible %} -
    -
    -   -
    - -
    -
    {{ data.upload_path }}
    -

    - {{ trans('csv.upload_not_writeable') }} -

    -
    -
    - {% endif %}
    - {% if data.is_upload_possible %} -
    -
    -
    -
    - -
    +
    +
    +
    +
    +
    - {% endif %} +
    diff --git a/resources/views/import/csv/map.twig b/resources/views/import/csv/map.twig index ebc0cf669d..0d58cfcf51 100644 --- a/resources/views/import/csv/map.twig +++ b/resources/views/import/csv/map.twig @@ -1,7 +1,7 @@ {% extends "./layout/default" %} {% block breadcrumbs %} - {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, job) }} {% endblock %} {% block content %} @@ -22,9 +22,8 @@
    -
    + - {% for field in data %}
    @@ -37,8 +36,8 @@ - - + + @@ -63,53 +62,12 @@ {% endfor %} - {# - - {% for index,columnName in map %} - -
    -
    -
    -
    -

    {{ Config.get('csv.roles.'~columnName~'.name') }}

    -
    -
    -
    {{ trans('csv.field_value') }}{{ trans('csv.field_mapped_to') }}{{ trans('csv.map_field_value') }}{{ trans('csv.map_field_mapped_to') }}
    - - - - - - - - {% for value in values[index] %} - - - - - {% endfor %} - - - -
    {{ 'csv_field_value'|_ }}{{ 'csv_field_mapped_to'|_ }}
    {{ value }} - {{ Form.select('mapping['~index~']['~value~']',options[index], mapped[index][value], {class: 'form-control'}) }} -
    - - -
    -
    -
    -
    - {% endfor %} - #} - -
    diff --git a/resources/views/import/csv/roles.twig b/resources/views/import/csv/roles.twig index 0df197187b..4513b1b7dd 100644 --- a/resources/views/import/csv/roles.twig +++ b/resources/views/import/csv/roles.twig @@ -1,7 +1,7 @@ {% extends "./layout/default" %} {% block breadcrumbs %} - {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, job) }} {% endblock %} {% block content %} @@ -10,18 +10,18 @@
    -

    {{ trans('csv.column_roles_title') }}

    +

    {{ trans('csv.roles_title') }}

    - {{ 'see_help_top_right'|_ }} + {{ trans('csv.roles_text') }}

    - + @@ -29,41 +29,41 @@
    -

    {{ trans('csv.column_roles_table') }}

    +

    {{ trans('csv.roles_table') }}

    - - - - + + + + - {% for i in 0..(data.columnCount-1) %} + {% for i in 0..(data.total -1) %} @@ -91,7 +91,7 @@
    diff --git a/resources/views/import/index.twig b/resources/views/import/index.twig index 922a3fa12e..c48d31ea6d 100644 --- a/resources/views/import/index.twig +++ b/resources/views/import/index.twig @@ -8,38 +8,34 @@
    -

    {{ 'import'|_ }}

    +

    {{ 'import_index_title'|_ }}

    -

    - {{ 'import_data_full'|_ }} -

    - {{ 'see_help_top_right'|_ }} + {{ 'import_index_intro'|_ }}

    -
    - {{ ExpandedForm.file('import_file', {helpText: 'import_file_help'|_}) }} - {{ ExpandedForm.file('configuration_file', {helpText: 'configuration_file_help'|_|raw}) }} - {{ ExpandedForm.select('import_file_type', importFileTypes, defaultImportType, {'helpText' : 'import_file_type_help'|_}) }} + {{ ExpandedForm.file('import_file', {helpText: 'import_index_file'|_}) }} + {{ ExpandedForm.file('configuration_file', {helpText: 'import_index_config'|_|raw}) }} + {{ ExpandedForm.select('import_file_type', importFileTypes, defaultImportType, {'helpText' : 'import_index_type'|_}) }}
    -
    diff --git a/resources/views/import/status.twig b/resources/views/import/status.twig index fbddfa8421..f1a8bcd5c6 100644 --- a/resources/views/import/status.twig +++ b/resources/views/import/status.twig @@ -4,50 +4,112 @@ {{ Breadcrumbs.renderIfExists }} {% endblock %} {% block content %} -
    + + {# Initial display. Will refresh (and disappear almost immediately. #} + +
    -

    {{ 'import_status_header'|_ }}

    +

    {{ 'import_status_wait_title'|_ }}

    -
    -
    -
    -
    -

    {{ 'import_status_settings_complete'|_ }}

    +

    + {{ 'import_status_wait_text'|_ }} +

    -
    + {# Fatal error display. Will be shown (duh) when something goes horribly wrong. #} + -
    + {# Box for when the job is ready to start #} + + + {# Box for when the job is running! #} + + + {# displays the finished status of the import. #} +
    {{ trans('csv.column_name') }}{{ trans('csv.column_example') }}{{ trans('csv.column_role') }}{{ trans('csv.do_map_value') }}{{ trans('csv.roles_column_name') }}{{ trans('csv.roles_column_example') }}{{ trans('csv.roles_column_role') }}{{ trans('csv.roles_do_map_value') }}
    - {% if data.columnHeaders[i] == '' %} - {{ trans('csv.column') }} #{{ loop.index }} + {% if data.headers[i] == '' %} + {{ trans('csv.roles_column') }} #{{ loop.index }} {% else %} - {{ data.columnHeaders[i] }} + {{ data.headers[i] }} {% endif %} - {% if data.columns[i]|length == 0 %} - {{ trans('csv.no_example_data') }} + {% if data.examples[i]|length == 0 %} + {{ trans('csv.roles_no_example_data') }} {% else %} - {% for example in data.columns[i] %} + {% for example in data.examples[i] %} {{ example }}
    {% endfor %} {% endif %}
    {{ Form.select(('role['~loop.index0~']'), - data.available_roles, + data.roles, job.configuration['column-roles'][loop.index0], {class: 'form-control'}) }}
    - - - - - - - - - - - {% for account in result.accounts %} - - - - - - - - {% endfor %} - - -
    {{ trans('list.name') }}
    - {{ account.name }} - {{ trans('firefly.'~account.accountType.type) }}
    diff --git a/resources/views/search/partials/budgets.twig b/resources/views/search/partials/budgets.twig deleted file mode 100644 index 5d6c4008d4..0000000000 --- a/resources/views/search/partials/budgets.twig +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - {% for budget in result.budgets %} - - - - - {% endfor %} - - -
    {{ trans('list.name') }}
    - {{ budget.name }} -
    diff --git a/resources/views/search/partials/categories.twig b/resources/views/search/partials/categories.twig deleted file mode 100644 index 7dfef5da15..0000000000 --- a/resources/views/search/partials/categories.twig +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - {% for category in result.categories %} - - - - - {% endfor %} - - -
    {{ trans('list.name') }}
    - {{ category.name }} -
    diff --git a/resources/views/search/partials/tags.twig b/resources/views/search/partials/tags.twig deleted file mode 100644 index fd4342c67b..0000000000 --- a/resources/views/search/partials/tags.twig +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - {% for tag in result.tags %} - - - - - - {% endfor %} - - -
    {{ trans('list.name') }}{{ trans('list.type') }}
    - {{ tag.tag }} - {{ ('tag'~tag.tagMode)|_ }}
    diff --git a/resources/views/search/partials/transactions-large.twig b/resources/views/search/partials/transactions-large.twig deleted file mode 100644 index 7502719e27..0000000000 --- a/resources/views/search/partials/transactions-large.twig +++ /dev/null @@ -1,139 +0,0 @@ -{{ journals.render|raw }} - - - - - - - - - - - - - {% if not hideBudgets %} - - {% endif %} - - - {% if not hideCategories %} - - {% endif %} - - - {% if not hideBills %} - - {% endif %} - - - - {% for transaction in journals %} - - - - - - - - - - - - {% if not hideBudgets %} - - {% endif %} - - - {% if not hideCategories %} - - {% endif %} - - - {% if not hideBills %} - - {% endif %} - - {% endfor %} - -
    {{ trans('list.description') }}{{ trans('list.amount') }}
    - - - {% if transaction.transaction_description|length > 0 %} - {{ transaction.transaction_description }} ({{ transaction.description }}) - {% else %} - {{ transaction.description }} - {% endif %} - - {{ splitJournalIndicator(transaction.journal_id) }} - - {% if transaction.transactionJournal.attachments|length > 0 %} - - {% endif %} - - - - {% if transaction.transaction_type_type == 'Transfer' %} - - {{ formatByCode(transaction.transaction_currency_code, steam_positive(transaction.transaction_amount)) }} - - {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} - {% else %} - - {{ formatByCode(transaction.transaction_currency_code, transaction.transaction_amount) }} - - {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} - {% endif %} - - -
    - -
    -
    - {{ journals.render|raw }} -
    -
    - diff --git a/resources/views/search/partials/transactions.twig b/resources/views/search/partials/transactions.twig deleted file mode 100644 index 587c9b432d..0000000000 --- a/resources/views/search/partials/transactions.twig +++ /dev/null @@ -1,80 +0,0 @@ -{{ journals.render|raw }} - - - - - - - - - - - - {% for transaction in transactions %} - - - - - - - - {% endfor %} - -
    {{ trans('list.description') }}{{ trans('list.amount') }}
    - - - {% if transaction.transaction_description|length > 0 %} - {{ transaction.transaction_description }} ({{ transaction.description }}) - {% else %} - {{ transaction.description }} - {% endif %} - - {{ splitJournalIndicator(transaction.journal_id) }} - - {% if transaction.transactionJournal.attachments|length > 0 %} - - {% endif %} - - - - {{ formatByCode(transaction.transaction_currency_code, transaction.transaction_amount) }} - - {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} - - -
    - -
    -
    - {{ journals.render|raw }} -
    -
    - diff --git a/resources/views/search/search.twig b/resources/views/search/search.twig new file mode 100644 index 0000000000..61e89915b4 --- /dev/null +++ b/resources/views/search/search.twig @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + {% for transaction in transactions %} + + + + + + + + + + + + + {% endfor %} + +
    {{ trans('list.description') }}{{ trans('list.amount') }}
    + + + {% if transaction.transaction_description|length > 0 %} + {{ transaction.transaction_description }} ({{ transaction.description }}) + {% else %} + {{ transaction.description }} + {% endif %} + + {{ splitJournalIndicator(transaction.journal_id) }} + + {% if transaction.transactionJournal.attachments|length > 0 %} + + {% endif %} + + + {{ transactionAmount(transaction) }} + +
    \ No newline at end of file diff --git a/resources/views/tags/show.twig b/resources/views/tags/show.twig index 3538800b03..6f7e0037a4 100644 --- a/resources/views/tags/show.twig +++ b/resources/views/tags/show.twig @@ -105,9 +105,9 @@
    - {% include 'list/journals-tasker' %} + {% include 'list/journals' %} - {% if periods %} + {% if periods.count > 0 %}

    @@ -125,7 +125,7 @@

    - {% if periods %} + {% if periods.count > 0 %}
    {% for period in periods %} {% if period.spent != 0 or period.earned != 0 %} @@ -157,7 +157,7 @@
    {% endif %}
    - {% if periods %} + {% if periods.count > 0 %}

    {{ 'showEverything'|_ }}

    diff --git a/resources/views/transactions/index.twig b/resources/views/transactions/index.twig index 4a72c0cb2f..65bfbf64e7 100644 --- a/resources/views/transactions/index.twig +++ b/resources/views/transactions/index.twig @@ -22,7 +22,7 @@

    {{ subTitle }}

    - {% include 'list.journals-tasker' with {'journals': journals} %} + {% include 'list.journals' with {'journals': journals} %} {% if periods.count > 0 %}

    diff --git a/resources/views/transactions/mass-delete.twig b/resources/views/transactions/mass-delete.twig index f3525c1d6c..864ea084ae 100644 --- a/resources/views/transactions/mass-delete.twig +++ b/resources/views/transactions/mass-delete.twig @@ -29,7 +29,7 @@   {{ trans('list.description') }} - {{ trans('list.amount') }} + {{ trans('list.total_amount') }} {{ trans('list.date') }} {{ trans('list.from') }} {{ trans('list.to') }} @@ -43,7 +43,7 @@ {{ journal.description }} - {{ journal|formatJournal }} + {{ journalAmount(journal) }} {{ journal.date.formatLocalized(monthAndDayFormat) }} diff --git a/resources/views/transactions/mass/edit.twig b/resources/views/transactions/mass/edit.twig index c0c2ccc123..a8bab41a7d 100644 --- a/resources/views/transactions/mass/edit.twig +++ b/resources/views/transactions/mass/edit.twig @@ -47,11 +47,20 @@

    - {{ journal.transactionCurrency.symbol }} + {{ journal.currency_symbol }} +
    - + {% if journal.foreign_amount %} + {# insert foreign data #} +
    + {{ journal.foreign_currency.symbol }} + + +
    + {% endif %} {# DATE #} diff --git a/resources/views/transactions/show.twig b/resources/views/transactions/show.twig index 3c086d1899..a6a7fc8876 100644 --- a/resources/views/transactions/show.twig +++ b/resources/views/transactions/show.twig @@ -36,14 +36,8 @@ {{ 'total_amount'|_ }} - {{ journal|formatJournal }} - {% if journal.hasMeta('foreign_amount') %} - {% if journal.transactiontype.type == 'Withdrawal' %} - ({{ formatAnything(foreignCurrency, journal.getMeta('foreign_amount')*-1) }}) - {% else %} - ({{ formatAnything(foreignCurrency, journal.getMeta('foreign_amount')) }}) - {% endif %} - {% endif %} + + {{ journalAmount(journal) }} @@ -304,9 +298,7 @@ - - {{ formatAnything(journal.transactionCurrency, transaction.source_account_before) }} - ⟶ {{ formatAnything(journal.transactionCurrency, transaction.source_account_after) }} + {{ formatSourceBefore(transaction) }} → {{ formatSourceAfter(transaction) }} {% if transaction.destination_account_type == 'Cash account' %} @@ -317,27 +309,10 @@ - - {{ formatAnything(journal.transactionCurrency, transaction.destination_account_before) }} - ⟶ {{ formatAnything(journal.transactionCurrency, transaction.destination_account_after) }} + {{ formatDestinationBefore(transaction) }} → {{ formatDestinationAfter(transaction) }} - {% if journal.transactiontype.type == 'Deposit' %} - - {{ formatAnything(journal.transactionCurrency, transaction.destination_amount) }} - - {% endif %} - {% if journal.transactiontype.type == 'Withdrawal' %} - - {{ formatAnything(journal.transactionCurrency, transaction.source_amount) }} - - {% endif %} - {% if journal.transactiontype.type == 'Transfer' %} - - - {{ formatAnythingPlain(journal.transactionCurrency, transaction.destination_amount) }} - - {% endif %} + {{ journalAmount(journal) }} {{ transactionIdBudgets(transaction.source_id) }} diff --git a/resources/views/transactions/single/create.twig b/resources/views/transactions/single/create.twig index a9425ea8db..43d74ab252 100644 --- a/resources/views/transactions/single/create.twig +++ b/resources/views/transactions/single/create.twig @@ -10,7 +10,7 @@
    -
    +

    {{ 'mandatoryFields'|_ }}

    @@ -66,7 +66,7 @@
    -
    +

    {{ 'optional_field_meta_data'|_ }}

    @@ -231,6 +231,10 @@ button['{{ type }}'] = '{{ trans('form.store_new_' ~ type) }}'; {% endfor %} + // some code for the foreign amount logic: + var useAccountCurrency = {% if preFilled.amount_currency_id_amount > 0 %}false{% else %}true{% endif %}; + var overruleCurrency = {{ preFilled.amount_currency_id_amount|default(0) }}; + diff --git a/resources/views/transactions/single/edit.twig b/resources/views/transactions/single/edit.twig index 4ef0d2ceaa..6e1aafe723 100644 --- a/resources/views/transactions/single/edit.twig +++ b/resources/views/transactions/single/edit.twig @@ -27,7 +27,7 @@
    -
    +

    {{ 'mandatoryFields'|_ }}

    @@ -64,9 +64,9 @@ {{ ExpandedForm.nonSelectableAmount('native_amount', data.native_amount, {currency: data.native_currency}) }} - {{ ExpandedForm.nonSelectableAmount('source_amount', data.native_amount, {currency: data.native_currency }) }} + {{ ExpandedForm.nonSelectableAmount('source_amount', data.source_amount, {currency: data.source_currency }) }} - {{ ExpandedForm.nonSelectableAmount('destination_amount', data.amount, {currency: data.currency }) }} + {{ ExpandedForm.nonSelectableAmount('destination_amount', data.destination_amount, {currency: data.destination_currency }) }} {# ALWAYS SHOW DATE #} {{ ExpandedForm.date('date',data['date']) }} @@ -74,7 +74,7 @@
    -
    +

    {{ 'optionalFields'|_ }}

    diff --git a/resources/views/transactions/split/edit.twig b/resources/views/transactions/split/edit.twig index e78dffb4ab..a1f9f915dc 100644 --- a/resources/views/transactions/split/edit.twig +++ b/resources/views/transactions/split/edit.twig @@ -30,7 +30,7 @@
    {% endif %}
    -
    +

    {{ 'transaction_data'|_ }}

    @@ -40,9 +40,6 @@ {# DESCRIPTION IS ALWAYS AVAILABLE #} {{ ExpandedForm.text('journal_description', journal.description) }} - {# CURRENCY IS NEW FOR SPLIT JOURNALS #} - {{ ExpandedForm.select('currency_id', currencies, preFilled.currency_id) }} - {# show source if withdrawal or transfer #} {% if preFilled.what == 'withdrawal' or preFilled.what == 'transfer' %} {{ ExpandedForm.select('journal_source_account_id', assetAccounts, preFilled.journal_source_account_id) }} @@ -59,6 +56,7 @@ {% endif %} {# TOTAL AMOUNT IS STATIC TEXT #} + {# TODO this does not reflect the actual currency (currencies) #} {{ ExpandedForm.staticText('journal_amount', preFilled.journal_amount|formatAmount ) }} @@ -67,7 +65,7 @@
    -
    +

    {{ 'optional_field_meta_data'|_ }}

    @@ -187,89 +185,108 @@

    {{ 'splits'|_ }}

    - - - - - - - +
    +
    +
     
    +
    {{ trans('list.split_number') }}
    +
    {{ trans('list.description') }}
    {# withdrawal and deposit have a destination. #} {% if preFilled.what == 'withdrawal' %} -
    +
    {{ trans('list.destination') }}
    {% endif %} - - {# DEPOSIT HAS A SOURCE #} + {# Deposit has a source #} {% if preFilled.what == 'deposit' %} - +
    {{ trans('list.source') }}
    + {% endif %} +
    {{ trans('list.amount') }}
    + {% if transaction.foreign_amount != null %} +
     
    {% endif %} - - - {# only withdrawal has budget #} {% if preFilled.what == 'withdrawal' %} - +
    {{ trans('list.budget') }}
    + {% endif %} +
    {{ trans('list.category') }}
    + + + {% for index, transaction in preFilled.transactions %} +
    + {# button #} +
    + + +
    + + {# index #} +
    #{{ loop.index }}
    + + {# description #} +
    + +
    + + {# destination for withdrawals: #} + {% if preFilled.what == 'withdrawal' %} +
    + +
    {% endif %} -
    - - - - {% for index, transaction in preFilled.transactions %} - - - - + {# source for deposits #} + {% if preFilled.what == 'deposit' %} +
    + +
    + {% endif %} - - {% if preFilled.what == 'withdrawal' %} - - {% endif %} - - - {% if preFilled.what == 'deposit' %} - - {% endif %} - - - + + + - {% if preFilled.what == 'withdrawal' %} - - {% endif %} - - - {% endfor %} - -
     {{ trans('list.split_number') }}{{ trans('list.description') }}{{ trans('list.destination') }}{{ trans('list.source') }}{{ trans('list.amount') }}{{ trans('list.budget') }}{{ trans('list.category') }}
    #{{ loop.index }} - - - - - - + {# amount#} +
    +
    +
    {{ transaction.transaction_currency_symbol }}
    -
    - - - -
    + {# foreign amount #} + {% if transaction.foreign_amount != null %} +
    +
    +
    {{ transaction.foreign_currency_symbol }}
    + +
    + +
    + {% endif %} + + {# budget #} + {% if preFilled.what == 'withdrawal' %} +
    + +
    + {% endif %} + + {# category #} +
    + +
    +
    + {% endfor %} +


    {{ 'add_another_split'|_ }} diff --git a/routes/api.php b/routes/api.php index f5c5fdb62b..ff61db1483 100755 --- a/routes/api.php +++ b/routes/api.php @@ -9,9 +9,7 @@ * See the LICENSE file for details. */ -declare(strict_types = 1); - -use Illuminate\Http\Request; +declare(strict_types=1); /* |-------------------------------------------------------------------------- diff --git a/routes/console.php b/routes/console.php index f0c08dc0c1..d03b78b097 100755 --- a/routes/console.php +++ b/routes/console.php @@ -9,7 +9,7 @@ * See the LICENSE file for details. */ -declare(strict_types = 1); +declare(strict_types=1); /* |-------------------------------------------------------------------------- diff --git a/routes/web.php b/routes/web.php index 4b480c3b1c..7680198dd0 100755 --- a/routes/web.php +++ b/routes/web.php @@ -44,6 +44,7 @@ Route::group( Route::get('error', ['uses' => 'HomeController@displayError', 'as' => 'error']); Route::any('logout', ['uses' => 'Auth\LoginController@logout', 'as' => 'logout']); Route::get('flush', ['uses' => 'HomeController@flush', 'as' => 'flush']); + Route::get('routes', ['uses' => 'HomeController@routes', 'as' => 'routes']); } ); @@ -76,6 +77,7 @@ Route::group( } ); + /** * Account Controller */ @@ -121,8 +123,8 @@ Route::group( Route::get('edit/{bill}', ['uses' => 'BillController@edit', 'as' => 'edit']); Route::get('delete/{bill}', ['uses' => 'BillController@delete', 'as' => 'delete']); Route::get('show/{bill}', ['uses' => 'BillController@show', 'as' => 'show']); - Route::post('store', ['uses' => 'BillController@store', 'as' => 'store']); + Route::post('store', ['uses' => 'BillController@store', 'as' => 'store']); Route::post('update/{bill}', ['uses' => 'BillController@update', 'as' => 'update']); Route::post('destroy/{bill}', ['uses' => 'BillController@destroy', 'as' => 'destroy']); } @@ -134,7 +136,6 @@ Route::group( */ Route::group( ['middleware' => 'user-full-auth', 'prefix' => 'budgets', 'as' => 'budgets.'], function () { - Route::get('', ['uses' => 'BudgetController@index', 'as' => 'index']); Route::get('income', ['uses' => 'BudgetController@updateIncome', 'as' => 'income']); Route::get('create', ['uses' => 'BudgetController@create', 'as' => 'create']); Route::get('edit/{budget}', ['uses' => 'BudgetController@edit', 'as' => 'edit']); @@ -142,6 +143,7 @@ Route::group( Route::get('show/{budget}', ['uses' => 'BudgetController@show', 'as' => 'show']); Route::get('show/{budget}/{budgetlimit}', ['uses' => 'BudgetController@showByBudgetLimit', 'as' => 'show.limit']); Route::get('list/no-budget/{moment?}', ['uses' => 'BudgetController@noBudget', 'as' => 'no-budget']); + Route::get('{moment?}', ['uses' => 'BudgetController@index', 'as' => 'index']); Route::post('income', ['uses' => 'BudgetController@postUpdateIncome', 'as' => 'income.post']); Route::post('store', ['uses' => 'BudgetController@store', 'as' => 'store']); @@ -380,21 +382,25 @@ Route::group( */ Route::group( ['middleware' => 'user-full-auth', 'prefix' => 'import', 'as' => 'import.'], function () { + Route::get('', ['uses' => 'ImportController@index', 'as' => 'index']); + Route::post('initialize', ['uses' => 'ImportController@initialize', 'as' => 'initialize']); + Route::get('configure/{importJob}', ['uses' => 'ImportController@configure', 'as' => 'configure']); - Route::get('settings/{importJob}', ['uses' => 'ImportController@settings', 'as' => 'settings']); - Route::get('complete/{importJob}', ['uses' => 'ImportController@complete', 'as' => 'complete']); + Route::post('configure/{importJob}', ['uses' => 'ImportController@postConfigure', 'as' => 'process-configuration']); + Route::get('download/{importJob}', ['uses' => 'ImportController@download', 'as' => 'download']); Route::get('status/{importJob}', ['uses' => 'ImportController@status', 'as' => 'status']); Route::get('json/{importJob}', ['uses' => 'ImportController@json', 'as' => 'json']); - Route::get('finished/{importJob}', ['uses' => 'ImportController@finished', 'as' => 'finished']); - - Route::post('upload', ['uses' => 'ImportController@upload', 'as' => 'upload']); - Route::post('configure/{importJob}', ['uses' => 'ImportController@postConfigure', 'as' => 'process-configuration']); - Route::post('settings/{importJob}', ['uses' => 'ImportController@postSettings', 'as' => 'post-settings']); Route::post('start/{importJob}', ['uses' => 'ImportController@start', 'as' => 'start']); + //Route::get('settings/{importJob}', ['uses' => 'ImportController@settings', 'as' => 'settings']); + //Route::get('complete/{importJob}', ['uses' => 'ImportController@complete', 'as' => 'complete']); + //Route::get('finished/{importJob}', ['uses' => 'ImportController@finished', 'as' => 'finished']); + //Route::post('settings/{importJob}', ['uses' => 'ImportController@postSettings', 'as' => 'post-settings']); + + } ); @@ -430,7 +436,6 @@ Route::group( 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.unpaid']); @@ -441,8 +446,6 @@ Route::group( 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']); - // currency conversion: Route::get('rate/{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'Json\ExchangeController@getRate', 'as' => 'rate']); @@ -636,7 +639,7 @@ Route::group( Route::group( ['middleware' => 'user-full-auth', 'prefix' => 'search', 'as' => 'search.'], function () { Route::get('', ['uses' => 'SearchController@index', 'as' => 'index']); - + Route::any('search', ['uses' => 'SearchController@search', 'as' => 'search']); } ); diff --git a/storage/database/.gitignore b/storage/database/.gitignore index 5382885af6..d6b7ef32c8 100644 --- a/storage/database/.gitignore +++ b/storage/database/.gitignore @@ -1,3 +1,2 @@ * !.gitignore -!databasecopy.sqlite \ No newline at end of file diff --git a/storage/database/databasecopy.sqlite b/storage/database/databasecopy.sqlite deleted file mode 100755 index bc987ad220..0000000000 Binary files a/storage/database/databasecopy.sqlite and /dev/null differ diff --git a/test.sh b/test.sh index 177dc25cb9..ea9dbe9ddf 100755 --- a/test.sh +++ b/test.sh @@ -87,8 +87,15 @@ then # call test data generation script $(which php) /sites/FF3/test-data/artisan generate:data local sqlite + + # also run upgrade routine: + $(which php) /sites/FF3/firefly-iii/artisan firefly:upgrade-database + # copy new database over backup (resets backup) cp $DATABASE $DATABASECOPY + + # copy new database to test-data repository: + cp $DATABASE /sites/FF3/test-data/storage/database.sqlite fi # do not reset database (optional) diff --git a/tests/Feature/Controllers/AccountControllerTest.php b/tests/Feature/Controllers/AccountControllerTest.php index d80c7d0917..fd68a3480f 100644 --- a/tests/Feature/Controllers/AccountControllerTest.php +++ b/tests/Feature/Controllers/AccountControllerTest.php @@ -132,7 +132,7 @@ class AccountControllerTest extends TestCase $journalRepos = $this->mock(JournalRepositoryInterface::class); $repository->shouldReceive('getAccountsByType')->andReturn(new Collection([$account])); $journalRepos->shouldReceive('first')->once()->andReturn(new TransactionJournal); - Steam::shouldReceive('balancesById')->andReturn([$account->id => '100']); + Steam::shouldReceive('balancesByAccounts')->andReturn([$account->id => '100']); Steam::shouldReceive('getLastActivities')->andReturn([]); $this->be($this->user()); diff --git a/tests/Feature/Controllers/Admin/ConfigurationControllerTest.php b/tests/Feature/Controllers/Admin/ConfigurationControllerTest.php index 44bad78766..a581937ea8 100644 --- a/tests/Feature/Controllers/Admin/ConfigurationControllerTest.php +++ b/tests/Feature/Controllers/Admin/ConfigurationControllerTest.php @@ -7,7 +7,7 @@ * See the LICENSE file for details. */ -declare(strict_types = 1); +declare(strict_types=1); namespace Tests\Feature\Controllers\Admin; diff --git a/tests/Feature/Controllers/Admin/HomeControllerTest.php b/tests/Feature/Controllers/Admin/HomeControllerTest.php index b31d833ff4..3602c0851f 100644 --- a/tests/Feature/Controllers/Admin/HomeControllerTest.php +++ b/tests/Feature/Controllers/Admin/HomeControllerTest.php @@ -7,7 +7,7 @@ * See the LICENSE file for details. */ -declare(strict_types = 1); +declare(strict_types=1); namespace Tests\Feature\Controllers\Admin; diff --git a/tests/Feature/Controllers/Admin/UserControllerTest.php b/tests/Feature/Controllers/Admin/UserControllerTest.php index d272442aba..4b656ebe79 100644 --- a/tests/Feature/Controllers/Admin/UserControllerTest.php +++ b/tests/Feature/Controllers/Admin/UserControllerTest.php @@ -81,9 +81,9 @@ class UserControllerTest extends TestCase $repository = $this->mock(UserRepositoryInterface::class); $repository->shouldReceive('changePassword')->once(); $repository->shouldReceive('changeStatus')->once(); - $data = [ + $data = [ 'id' => 1, - 'email' => 'test@example.com', + 'email' => 'test@example.com', 'password' => 'james', 'password_confirmation' => 'james', 'blocked_code' => 'blocked', diff --git a/tests/Feature/Controllers/AttachmentControllerTest.php b/tests/Feature/Controllers/AttachmentControllerTest.php index 271767907a..51d2eddcca 100644 --- a/tests/Feature/Controllers/AttachmentControllerTest.php +++ b/tests/Feature/Controllers/AttachmentControllerTest.php @@ -7,7 +7,7 @@ * See the LICENSE file for details. */ -declare(strict_types = 1); +declare(strict_types=1); namespace Tests\Feature\Controllers; diff --git a/tests/Feature/Controllers/Auth/ForgotPasswordControllerTest.php b/tests/Feature/Controllers/Auth/ForgotPasswordControllerTest.php index 34e1d7e36a..7d180e5b95 100644 --- a/tests/Feature/Controllers/Auth/ForgotPasswordControllerTest.php +++ b/tests/Feature/Controllers/Auth/ForgotPasswordControllerTest.php @@ -48,4 +48,4 @@ class ForgotPasswordControllerTest extends TestCase $response = $this->post(route('password.email'), $data); $response->assertStatus(302); } -} \ No newline at end of file +} diff --git a/tests/Feature/Controllers/Auth/TwoFactorControllerTest.php b/tests/Feature/Controllers/Auth/TwoFactorControllerTest.php index ef7731b512..5c268d3560 100644 --- a/tests/Feature/Controllers/Auth/TwoFactorControllerTest.php +++ b/tests/Feature/Controllers/Auth/TwoFactorControllerTest.php @@ -101,7 +101,7 @@ class TwoFactorControllerTest extends TestCase */ public function testPostIndex() { - $data = ['code' => '123456']; + $data = ['code' => '123456']; $google = $this->mock(Google2FA::class); $google->shouldReceive('verifyKey')->andReturn(true)->once(); diff --git a/tests/Feature/Controllers/BillControllerTest.php b/tests/Feature/Controllers/BillControllerTest.php index 22296e6820..9b5b6ca88b 100644 --- a/tests/Feature/Controllers/BillControllerTest.php +++ b/tests/Feature/Controllers/BillControllerTest.php @@ -7,7 +7,7 @@ * See the LICENSE file for details. */ -declare(strict_types = 1); +declare(strict_types=1); namespace Tests\Feature\Controllers; diff --git a/tests/Feature/Controllers/BudgetControllerTest.php b/tests/Feature/Controllers/BudgetControllerTest.php index b79a537f54..0e707af8d4 100644 --- a/tests/Feature/Controllers/BudgetControllerTest.php +++ b/tests/Feature/Controllers/BudgetControllerTest.php @@ -7,7 +7,7 @@ * See the LICENSE file for details. */ -declare(strict_types = 1); +declare(strict_types=1); namespace Tests\Feature\Controllers; @@ -166,6 +166,86 @@ class BudgetControllerTest extends TestCase $response->assertSee('