diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000000..69765bff51 --- /dev/null +++ b/.env.docker @@ -0,0 +1,55 @@ +APP_ENV=${FF_APP_ENV} +APP_DEBUG=false +APP_FORCE_SSL=false +APP_FORCE_ROOT= +APP_KEY=${FF_APP_KEY} +APP_LOG=daily +APP_LOG_LEVEL=warning +APP_URL=http://localhost + +DB_CONNECTION=mysql +DB_HOST=${FF_DB_HOST} +DB_PORT=3306 +DB_DATABASE=${FF_DB_NAME} +DB_USERNAME=${FF_DB_USER} +DB_PASSWORD=${FF_DB_PASSWORD} + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +SESSION_DRIVER=file +QUEUE_DRIVER=sync + +COOKIE_PATH="/" +COOKIE_DOMAIN= +COOKIE_SECURE=false + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_DRIVER=smtp +MAIL_HOST=mailtrap.io +MAIL_PORT=2525 +MAIL_FROM=changeme@example.com +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null + +SEND_REGISTRATION_MAIL=true +SEND_ERROR_MESSAGE=true +SHOW_INCOMPLETE_TRANSLATIONS=false + +CACHE_PREFIX=firefly + +GOOGLE_MAPS_API_KEY= +ANALYTICS_ID= +SITE_OWNER=mail@example.com +USE_ENCRYPTION=true + +PUSHER_KEY= +PUSHER_SECRET= +PUSHER_APP_ID= + +DEMO_USERNAME= +DEMO_PASSWORD= + diff --git a/.env.example b/.env.example index c628064094..eaa69b5d82 100755 --- a/.env.example +++ b/.env.example @@ -3,6 +3,7 @@ APP_DEBUG=false APP_FORCE_SSL=false APP_FORCE_ROOT= APP_KEY=SomeRandomStringOf32CharsExactly +APP_LOG=daily APP_LOG_LEVEL=warning APP_URL=http://localhost @@ -35,13 +36,20 @@ MAIL_PASSWORD=null MAIL_ENCRYPTION=null SEND_REGISTRATION_MAIL=true -MUST_CONFIRM_ACCOUNT=false - +SEND_ERROR_MESSAGE=true SHOW_INCOMPLETE_TRANSLATIONS=false +CACHE_PREFIX=firefly + +GOOGLE_MAPS_API_KEY= ANALYTICS_ID= SITE_OWNER=mail@example.com +USE_ENCRYPTION=true PUSHER_KEY= PUSHER_SECRET= PUSHER_APP_ID= + +DEMO_USERNAME= +DEMO_PASSWORD= + diff --git a/.env.sandstorm b/.env.sandstorm new file mode 100755 index 0000000000..ed8d9d511b --- /dev/null +++ b/.env.sandstorm @@ -0,0 +1,55 @@ +APP_ENV=production +APP_DEBUG=true +APP_FORCE_SSL=false +APP_FORCE_ROOT= +APP_KEY=SomeRandomStringOf32CharsExactly +APP_LOG=syslog +APP_LOG_LEVEL=debug +APP_URL=http://localhost + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=firefly +DB_USERNAME=firefly +DB_PASSWORD=firefly + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +SESSION_DRIVER=file +QUEUE_DRIVER=sync + +COOKIE_PATH="/" +COOKIE_DOMAIN= +COOKIE_SECURE=false + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_DRIVER=smtp +MAIL_HOST=mailtrap.io +MAIL_PORT=2525 +MAIL_FROM=changeme@example.com +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null + +SEND_REGISTRATION_MAIL=true +SEND_ERROR_MESSAGE=true +SHOW_INCOMPLETE_TRANSLATIONS=false + +CACHE_PREFIX=firefly + +GOOGLE_MAPS_API_KEY= +ANALYTICS_ID= +SITE_OWNER=mail@example.com +USE_ENCRYPTION=true + +PUSHER_KEY= +PUSHER_SECRET= +PUSHER_APP_ID= + +DEMO_USERNAME= +DEMO_PASSWORD= + diff --git a/.env.testing b/.env.testing new file mode 100755 index 0000000000..3c9802e829 --- /dev/null +++ b/.env.testing @@ -0,0 +1,45 @@ +APP_ENV=testing +APP_DEBUG=true +APP_FORCE_SSL=false +APP_FORCE_ROOT= +APP_KEY=TestTestTestTestTestTestTestTest +APP_LOG_LEVEL=debug +APP_URL=http://localhost + +DB_CONNECTION=sqlite +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_USERNAME=homestead +DB_PASSWORD=secret + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +SESSION_DRIVER=file +QUEUE_DRIVER=sync + +COOKIE_PATH="/" +COOKIE_DOMAIN= +COOKIE_SECURE=false + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_DRIVER=smtp +MAIL_HOST=mailtrap.io +MAIL_PORT=2525 +MAIL_FROM=changeme@example.com +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null + +SEND_REGISTRATION_MAIL=true +SEND_ERROR_MESSAGE=true +SHOW_INCOMPLETE_TRANSLATIONS=false + +ANALYTICS_ID= +SITE_OWNER=mail@example.com + +PUSHER_KEY= +PUSHER_SECRET= +PUSHER_APP_ID= diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000000..612661a00f --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Welcome to Firefly III on Github! + +:+1::tada: Thank you for taking the time to contribute something to Firefly III! + +## 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/). + +## 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. + +## 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! + +## Pull requests + +I can only accept pull requests against the `develop` branch, never the `master` branch. diff --git a/.gitignore b/.gitignore index 31a213bd48..6b366227eb 100755 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ result.html test-import.sh test-import-report.txt public/google*.html +.env.backup diff --git a/.sandstorm/.gitattributes b/.sandstorm/.gitattributes new file mode 100644 index 0000000000..5a533b9f62 --- /dev/null +++ b/.sandstorm/.gitattributes @@ -0,0 +1,5 @@ + + +# vagrant-spk creates shell scripts, which must end in \n, even on a \r\n system. +*.sh text eol=lf + diff --git a/.sandstorm/.gitignore b/.sandstorm/.gitignore new file mode 100644 index 0000000000..d70e1e39e4 --- /dev/null +++ b/.sandstorm/.gitignore @@ -0,0 +1,5 @@ + + +# This file stores a list of sub-paths of .sandstorm/ that should be ignored by git. +.vagrant + diff --git a/.sandstorm/Vagrantfile b/.sandstorm/Vagrantfile new file mode 100644 index 0000000000..20c01b674b --- /dev/null +++ b/.sandstorm/Vagrantfile @@ -0,0 +1,103 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# Guess at a reasonable name for the VM based on the folder vagrant-spk is +# run from. The timestamp is there to avoid conflicts if you have multiple +# folders with the same name. +VM_NAME = File.basename(File.dirname(File.dirname(__FILE__))) + "_sandstorm_#{Time.now.utc.to_i}" + +# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! +VAGRANTFILE_API_VERSION = "2" + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + # Base on the Sandstorm snapshots of the official Debian 8 (jessie) box. + config.vm.box = "sandstorm/debian-jessie64" + + if Vagrant.has_plugin?("vagrant-vbguest") then + # vagrant-vbguest is a Vagrant plugin that upgrades + # the version of VirtualBox Guest Additions within each + # guest. If you have the vagrant-vbguest plugin, then it + # needs to know how to compile kernel modules, etc., and so + # we give it this hint about operating system type. + config.vm.guest = "debian" + end + + # We forward port 6080, the Sandstorm web port, so that developers can + # visit their sandstorm app from their browser as local.sandstorm.io:6080 + # (aka 127.0.0.1:6080). + config.vm.network :forwarded_port, guest: 6080, host: 6080 + + # Use a shell script to "provision" the box. This installs Sandstorm using + # the bundled installer. + config.vm.provision "shell", inline: "sudo bash /opt/app/.sandstorm/global-setup.sh", keep_color: true + # Then, do stack-specific and app-specific setup. + config.vm.provision "shell", inline: "sudo bash /opt/app/.sandstorm/setup.sh", keep_color: true + + # Shared folders are configured per-provider since vboxsf can't handle >4096 open files, + # NFS requires privilege escalation every time you bring a VM up, + # and 9p is only available on libvirt. + + # Calculate the number of CPUs and the amount of RAM the system has, + # in a platform-dependent way; further logic below. + cpus = nil + total_kB_ram = nil + + host = RbConfig::CONFIG['host_os'] + if host =~ /darwin/ + cpus = `sysctl -n hw.ncpu`.to_i + total_kB_ram = `sysctl -n hw.memsize`.to_i / 1024 + elsif host =~ /linux/ + cpus = `nproc`.to_i + total_kB_ram = `grep MemTotal /proc/meminfo | awk '{print $2}'`.to_i + elsif host =~ /mingw/ + # powershell may not be available on Windows XP and Vista, so wrap this in a rescue block + begin + cpus = `powershell -Command "(Get-WmiObject Win32_Processor -Property NumberOfLogicalProcessors | Select-Object -Property NumberOfLogicalProcessors | Measure-Object NumberOfLogicalProcessors -Sum).Sum"`.to_i + total_kB_ram = `powershell -Command "Get-CimInstance -class cim_physicalmemory | % $_.Capacity}"`.to_i / 1024 + rescue + end + end + # Use the same number of CPUs within Vagrant as the system, with 1 + # as a default. + # + # Use at least 512MB of RAM, and if the system has more than 2GB of + # RAM, use 1/4 of the system RAM. This seems a reasonable compromise + # between having the Vagrant guest operating system not run out of + # RAM entirely (which it basically would if we went much lower than + # 512MB) and also allowing it to use up a healthily large amount of + # RAM so it can run faster on systems that can afford it. + if cpus.nil? or cpus.zero? + cpus = 1 + end + if total_kB_ram.nil? or total_kB_ram < 2048000 + assign_ram_mb = 512 + else + assign_ram_mb = (total_kB_ram / 1024 / 4) + end + # Actually apply these CPU/memory values to the providers. + config.vm.provider :virtualbox do |vb, override| + vb.cpus = cpus + vb.memory = assign_ram_mb + vb.name = VM_NAME + vb.customize ["modifyvm", :id, "--nictype1", "Am79C973"] + + # /opt/app and /host-dot-sandstorm are used by vagrant-spk + override.vm.synced_folder "..", "/opt/app" + override.vm.synced_folder ENV["HOME"] + "/.sandstorm", "/host-dot-sandstorm" + # /vagrant is not used by vagrant-spk; we need this line so it gets disabled; if we removed the + # line, vagrant would automatically insert a synced folder in /vagrant, which is not what we want. + override.vm.synced_folder "..", "/vagrant", disabled: true + end + config.vm.provider :libvirt do |libvirt, override| + libvirt.cpus = cpus + libvirt.memory = assign_ram_mb + libvirt.default_prefix = VM_NAME + + # /opt/app and /host-dot-sandstorm are used by vagrant-spk + override.vm.synced_folder "..", "/opt/app", type: "9p", accessmode: "passthrough" + override.vm.synced_folder ENV["HOME"] + "/.sandstorm", "/host-dot-sandstorm", type: "9p", accessmode: "passthrough" + # /vagrant is not used by vagrant-spk; we need this line so it gets disabled; if we removed the + # line, vagrant would automatically insert a synced folder in /vagrant, which is not what we want. + override.vm.synced_folder "..", "/vagrant", type: "9p", accessmode: "passthrough", disabled: true + end +end diff --git a/.sandstorm/app-graphics/firefly-iii-128.png b/.sandstorm/app-graphics/firefly-iii-128.png new file mode 100644 index 0000000000..70de2bc75a Binary files /dev/null and b/.sandstorm/app-graphics/firefly-iii-128.png differ diff --git a/.sandstorm/app-graphics/firefly-iii-150.png b/.sandstorm/app-graphics/firefly-iii-150.png new file mode 100644 index 0000000000..0d97be4768 Binary files /dev/null and b/.sandstorm/app-graphics/firefly-iii-150.png differ diff --git a/.sandstorm/app-graphics/firefly-iii-24.png b/.sandstorm/app-graphics/firefly-iii-24.png new file mode 100644 index 0000000000..a727ae4992 Binary files /dev/null and b/.sandstorm/app-graphics/firefly-iii-24.png differ diff --git a/.sandstorm/app-graphics/firefly-iii-48.png b/.sandstorm/app-graphics/firefly-iii-48.png new file mode 100644 index 0000000000..6f81c5731b Binary files /dev/null and b/.sandstorm/app-graphics/firefly-iii-48.png differ diff --git a/.sandstorm/build.sh b/.sandstorm/build.sh new file mode 100755 index 0000000000..93daf1ba86 --- /dev/null +++ b/.sandstorm/build.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Checks if there's a composer.json, and if so, installs/runs composer. +# This script only runs once, when the app connects to sandstorm. +set -euo pipefail + + + +cd /opt/app + +cp .env.sandstorm .env + +if [ -f /opt/app/composer.json ] ; then + if [ ! -f composer.phar ] ; then + curl -sS https://getcomposer.org/installer | php + fi + php composer.phar install --no-dev --no-suggest +fi + +# link storage folder +rm -rf /opt/app/storage +ln -s /var/storage /opt/app \ No newline at end of file diff --git a/.sandstorm/changelog.md b/.sandstorm/changelog.md new file mode 100644 index 0000000000..58249de976 --- /dev/null +++ b/.sandstorm/changelog.md @@ -0,0 +1,3 @@ +# 3.4.3 + +* Initial release on Sandstorm.io \ No newline at end of file diff --git a/.sandstorm/description.md b/.sandstorm/description.md new file mode 100644 index 0000000000..e9f6874f3f --- /dev/null +++ b/.sandstorm/description.md @@ -0,0 +1,3 @@ +"Firefly III" is a financial manager. It can help you keep track of expenses, income, budgets and everything in between. It even supports credit cards, shared household accounts and savings accounts! It’s pretty fancy. You should use it to save and organise money. + +Firefly works on the principle that if you know where you’re money is going, you can stop it from going there. \ No newline at end of file diff --git a/.sandstorm/global-setup.sh b/.sandstorm/global-setup.sh new file mode 100755 index 0000000000..219c770f63 --- /dev/null +++ b/.sandstorm/global-setup.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +# Set options for curl. Since we only want to show errors from these curl commands, we also use +# 'cat' to buffer the output; for more information: +# https://github.com/sandstorm-io/vagrant-spk/issues/158 + +CURL_OPTS="--silent --show-error" +echo localhost > /etc/hostname +hostname localhost + +# The following line copies stderr through stderr to cat without accidentally leaving it in the +# output file. Be careful when changing. See: https://github.com/sandstorm-io/vagrant-spk/pull/159 +curl $CURL_OPTS https://install.sandstorm.io/ 2>&1 > /host-dot-sandstorm/caches/install.sh | cat + +SANDSTORM_CURRENT_VERSION=$(curl $CURL_OPTS -f "https://install.sandstorm.io/dev?from=0&type=install") +SANDSTORM_PACKAGE="sandstorm-$SANDSTORM_CURRENT_VERSION.tar.xz" +if [[ ! -f /host-dot-sandstorm/caches/$SANDSTORM_PACKAGE ]] ; then + echo -n "Downloading Sandstorm version ${SANDSTORM_CURRENT_VERSION}..." + curl $CURL_OPTS --output "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE.partial" "https://dl.sandstorm.io/$SANDSTORM_PACKAGE" 2>&1 | cat + mv "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE.partial" "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE" + echo "...done." +fi +if [ ! -e /opt/sandstorm/latest/sandstorm ] ; then + echo -n "Installing Sandstorm version ${SANDSTORM_CURRENT_VERSION}..." + bash /host-dot-sandstorm/caches/install.sh -d -e "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE" >/dev/null + echo "...done." +fi +modprobe ip_tables +# Make the vagrant user part of the sandstorm group so that commands like +# `spk dev` work. +usermod -a -G 'sandstorm' 'vagrant' +# Bind to all addresses, so the vagrant port-forward works. +sudo sed --in-place='' \ + --expression='s/^BIND_IP=.*/BIND_IP=0.0.0.0/' \ + /opt/sandstorm/sandstorm.conf +sudo service sandstorm restart +# Enable apt-cacher-ng proxy to make things faster if one appears to be running on the gateway IP +GATEWAY_IP=$(ip route | grep ^default | cut -d ' ' -f 3) +if nc -z "$GATEWAY_IP" 3142 ; then + echo "Acquire::http::Proxy \"http://$GATEWAY_IP:3142\";" > /etc/apt/apt.conf.d/80httpproxy +fi +# Configure apt to retry fetching things that fail to download. +echo "APT::Acquire::Retries \"10\";" > /etc/apt/apt.conf.d/80sandstorm-retry diff --git a/.sandstorm/launcher.sh b/.sandstorm/launcher.sh new file mode 100755 index 0000000000..f6fb9c13bc --- /dev/null +++ b/.sandstorm/launcher.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Runs every time we create a new grain! + +# Create a bunch of folders under the clean /var that php, nginx, and mysql expect to exist +mkdir -p /var/lib/mysql +mkdir -p /var/lib/nginx +mkdir -p /var/lib/php/sessions/ +mkdir -p /var/log +mkdir -p /var/log/mysql +mkdir -p /var/log/nginx +# Wipe /var/run, since pidfiles and socket files from previous launches should go away +# TODO someday: I'd prefer a tmpfs for these. +rm -rf /var/run +mkdir -p /var/run +rm -rf /var/tmp +mkdir -p /var/tmp +mkdir -p /var/run/mysqld + +# make storage directories +rm -rf /var/storage +mkdir -p /var/storage/app/public +mkdir -p /var/storage/build +mkdir -p /var/storage/database +mkdir -p /var/storage/debugbar +mkdir -p /var/storage/export +mkdir -p /var/storage/framework/cache +mkdir -p /var/storage/framework/sessions +mkdir -p /var/storage/framework/views +mkdir -p /var/storage/logs +mkdir -p /var/storage/upload + + +# Ensure mysql tables created +HOME=/etc/mysql /usr/bin/mysql_install_db --force + +# Spawn mysqld, php +HOME=/etc/mysql /usr/sbin/mysqld & + +/usr/sbin/php-fpm7.0 --nodaemonize --fpm-config /etc/php/7.0/fpm/php-fpm.conf & + +# Wait until mysql and php have bound their sockets, indicating readiness +while [ ! -e /var/run/mysqld/mysqld.sock ] ; do + echo "waiting for mysql to be available at /var/run/mysqld/mysqld.sock" + sleep .5 +done +while [ ! -e /var/run/php7.0-fpm.sock ] ; do + echo "waiting for php7.0-fpm to be available at /var/run/php7.0-fpm.sock" + sleep .5 +done + +echo "Installing database.." +# Install database for Firefly III +echo "CREATE DATABASE IF NOT EXISTS firefly; GRANT ALL on firefly.* TO 'firefly'@'localhost' IDENTIFIED BY 'firefly';" | mysql -uroot +echo "Done!" + +echo "Migrating..." +php /opt/app/artisan migrate --seed --force +echo "Done!" + +# Start nginx. +/usr/sbin/nginx -c /opt/app/.sandstorm/service-config/nginx.conf -g "daemon off;" diff --git a/.sandstorm/pgp-keyring b/.sandstorm/pgp-keyring new file mode 100644 index 0000000000..b0dec3c169 Binary files /dev/null and b/.sandstorm/pgp-keyring differ diff --git a/.sandstorm/pgp-signature b/.sandstorm/pgp-signature new file mode 100644 index 0000000000..82e5fc8a52 Binary files /dev/null and b/.sandstorm/pgp-signature differ diff --git a/.sandstorm/sandstorm-files.list b/.sandstorm/sandstorm-files.list new file mode 100644 index 0000000000..91b1256d53 --- /dev/null +++ b/.sandstorm/sandstorm-files.list @@ -0,0 +1,1430 @@ +# *** WARNING: GENERATED FILE *** +# This file is automatically updated and rewritten in sorted order every time +# the app runs in dev mode. You may manually add or remove files, but don't +# expect comments or ordering to be retained. +bin/bash +bin/cat +bin/chmod +bin/cp +bin/dash +bin/grep +bin/hostname +bin/ln +bin/ls +bin/mkdir +bin/rm +bin/sed +bin/sh +bin/sleep +bin/stty +etc/alternatives/php +etc/bash.bashrc +etc/bindresvport.blacklist +etc/default/nss +etc/hosts.allow +etc/hosts.deny +etc/inputrc +etc/ld.so.cache +etc/locale.alias +etc/localtime +etc/mysql/conf.d +etc/mysql/conf.d/mysqld_safe_syslog.cnf +etc/mysql/conf.d/sandstorm.cnf +etc/mysql/my.cnf +etc/php/7.0/cli/conf.d +etc/php/7.0/cli/conf.d/10-mysqlnd.ini +etc/php/7.0/cli/conf.d/10-opcache.ini +etc/php/7.0/cli/conf.d/10-pdo.ini +etc/php/7.0/cli/conf.d/15-xml.ini +etc/php/7.0/cli/conf.d/20-bcmath.ini +etc/php/7.0/cli/conf.d/20-calendar.ini +etc/php/7.0/cli/conf.d/20-ctype.ini +etc/php/7.0/cli/conf.d/20-curl.ini +etc/php/7.0/cli/conf.d/20-dom.ini +etc/php/7.0/cli/conf.d/20-exif.ini +etc/php/7.0/cli/conf.d/20-fileinfo.ini +etc/php/7.0/cli/conf.d/20-ftp.ini +etc/php/7.0/cli/conf.d/20-gettext.ini +etc/php/7.0/cli/conf.d/20-iconv.ini +etc/php/7.0/cli/conf.d/20-intl.ini +etc/php/7.0/cli/conf.d/20-json.ini +etc/php/7.0/cli/conf.d/20-mbstring.ini +etc/php/7.0/cli/conf.d/20-mysqli.ini +etc/php/7.0/cli/conf.d/20-pdo_mysql.ini +etc/php/7.0/cli/conf.d/20-phar.ini +etc/php/7.0/cli/conf.d/20-posix.ini +etc/php/7.0/cli/conf.d/20-readline.ini +etc/php/7.0/cli/conf.d/20-shmop.ini +etc/php/7.0/cli/conf.d/20-simplexml.ini +etc/php/7.0/cli/conf.d/20-sockets.ini +etc/php/7.0/cli/conf.d/20-sysvmsg.ini +etc/php/7.0/cli/conf.d/20-sysvsem.ini +etc/php/7.0/cli/conf.d/20-sysvshm.ini +etc/php/7.0/cli/conf.d/20-tokenizer.ini +etc/php/7.0/cli/conf.d/20-wddx.ini +etc/php/7.0/cli/conf.d/20-xmlreader.ini +etc/php/7.0/cli/conf.d/20-xmlwriter.ini +etc/php/7.0/cli/conf.d/20-xsl.ini +etc/php/7.0/cli/php.ini +etc/php/7.0/fpm/conf.d +etc/php/7.0/fpm/conf.d/10-mysqlnd.ini +etc/php/7.0/fpm/conf.d/10-opcache.ini +etc/php/7.0/fpm/conf.d/10-pdo.ini +etc/php/7.0/fpm/conf.d/15-xml.ini +etc/php/7.0/fpm/conf.d/20-bcmath.ini +etc/php/7.0/fpm/conf.d/20-calendar.ini +etc/php/7.0/fpm/conf.d/20-ctype.ini +etc/php/7.0/fpm/conf.d/20-curl.ini +etc/php/7.0/fpm/conf.d/20-dom.ini +etc/php/7.0/fpm/conf.d/20-exif.ini +etc/php/7.0/fpm/conf.d/20-fileinfo.ini +etc/php/7.0/fpm/conf.d/20-ftp.ini +etc/php/7.0/fpm/conf.d/20-gettext.ini +etc/php/7.0/fpm/conf.d/20-iconv.ini +etc/php/7.0/fpm/conf.d/20-intl.ini +etc/php/7.0/fpm/conf.d/20-json.ini +etc/php/7.0/fpm/conf.d/20-mbstring.ini +etc/php/7.0/fpm/conf.d/20-mysqli.ini +etc/php/7.0/fpm/conf.d/20-pdo_mysql.ini +etc/php/7.0/fpm/conf.d/20-phar.ini +etc/php/7.0/fpm/conf.d/20-posix.ini +etc/php/7.0/fpm/conf.d/20-readline.ini +etc/php/7.0/fpm/conf.d/20-shmop.ini +etc/php/7.0/fpm/conf.d/20-simplexml.ini +etc/php/7.0/fpm/conf.d/20-sockets.ini +etc/php/7.0/fpm/conf.d/20-sysvmsg.ini +etc/php/7.0/fpm/conf.d/20-sysvsem.ini +etc/php/7.0/fpm/conf.d/20-sysvshm.ini +etc/php/7.0/fpm/conf.d/20-tokenizer.ini +etc/php/7.0/fpm/conf.d/20-wddx.ini +etc/php/7.0/fpm/conf.d/20-xmlreader.ini +etc/php/7.0/fpm/conf.d/20-xmlwriter.ini +etc/php/7.0/fpm/conf.d/20-xsl.ini +etc/php/7.0/fpm/php-fpm.conf +etc/php/7.0/fpm/php.ini +etc/php/7.0/fpm/pool.d +etc/php/7.0/fpm/pool.d/www.conf +etc/php/7.0/mods-available/bcmath.ini +etc/php/7.0/mods-available/calendar.ini +etc/php/7.0/mods-available/ctype.ini +etc/php/7.0/mods-available/curl.ini +etc/php/7.0/mods-available/dom.ini +etc/php/7.0/mods-available/exif.ini +etc/php/7.0/mods-available/fileinfo.ini +etc/php/7.0/mods-available/ftp.ini +etc/php/7.0/mods-available/gettext.ini +etc/php/7.0/mods-available/iconv.ini +etc/php/7.0/mods-available/intl.ini +etc/php/7.0/mods-available/json.ini +etc/php/7.0/mods-available/mbstring.ini +etc/php/7.0/mods-available/mysqli.ini +etc/php/7.0/mods-available/mysqlnd.ini +etc/php/7.0/mods-available/opcache.ini +etc/php/7.0/mods-available/pdo.ini +etc/php/7.0/mods-available/pdo_mysql.ini +etc/php/7.0/mods-available/phar.ini +etc/php/7.0/mods-available/posix.ini +etc/php/7.0/mods-available/readline.ini +etc/php/7.0/mods-available/shmop.ini +etc/php/7.0/mods-available/simplexml.ini +etc/php/7.0/mods-available/sockets.ini +etc/php/7.0/mods-available/sysvmsg.ini +etc/php/7.0/mods-available/sysvsem.ini +etc/php/7.0/mods-available/sysvshm.ini +etc/php/7.0/mods-available/tokenizer.ini +etc/php/7.0/mods-available/wddx.ini +etc/php/7.0/mods-available/xml.ini +etc/php/7.0/mods-available/xmlreader.ini +etc/php/7.0/mods-available/xmlwriter.ini +etc/php/7.0/mods-available/xsl.ini +etc/services +etc/ssl/openssl.cnf +lib/terminfo/d/dumb +lib/x86_64-linux-gnu/ld-2.19.so +lib/x86_64-linux-gnu/libacl.so.1 +lib/x86_64-linux-gnu/libacl.so.1.1.0 +lib/x86_64-linux-gnu/libaio.so.1 +lib/x86_64-linux-gnu/libaio.so.1.0.1 +lib/x86_64-linux-gnu/libattr.so.1 +lib/x86_64-linux-gnu/libattr.so.1.1.0 +lib/x86_64-linux-gnu/libaudit.so.1 +lib/x86_64-linux-gnu/libaudit.so.1.0.0 +lib/x86_64-linux-gnu/libbsd.so.0 +lib/x86_64-linux-gnu/libbsd.so.0.7.0 +lib/x86_64-linux-gnu/libbz2.so.1.0 +lib/x86_64-linux-gnu/libbz2.so.1.0.4 +lib/x86_64-linux-gnu/libc-2.19.so +lib/x86_64-linux-gnu/libc.so.6 +lib/x86_64-linux-gnu/libcom_err.so.2 +lib/x86_64-linux-gnu/libcom_err.so.2.1 +lib/x86_64-linux-gnu/libcrypt-2.19.so +lib/x86_64-linux-gnu/libcrypt.so.1 +lib/x86_64-linux-gnu/libdl-2.19.so +lib/x86_64-linux-gnu/libdl.so.2 +lib/x86_64-linux-gnu/libexpat.so.1 +lib/x86_64-linux-gnu/libexpat.so.1.6.0 +lib/x86_64-linux-gnu/libgcc_s.so.1 +lib/x86_64-linux-gnu/libgcrypt.so.20 +lib/x86_64-linux-gnu/libgcrypt.so.20.0.3 +lib/x86_64-linux-gnu/libgpg-error.so.0 +lib/x86_64-linux-gnu/libgpg-error.so.0.13.0 +lib/x86_64-linux-gnu/libjson-c.so.2 +lib/x86_64-linux-gnu/libjson-c.so.2.0.0 +lib/x86_64-linux-gnu/libkeyutils.so.1 +lib/x86_64-linux-gnu/libkeyutils.so.1.5 +lib/x86_64-linux-gnu/liblzma.so.5 +lib/x86_64-linux-gnu/liblzma.so.5.0.0 +lib/x86_64-linux-gnu/libm-2.19.so +lib/x86_64-linux-gnu/libm.so.6 +lib/x86_64-linux-gnu/libncurses.so.5 +lib/x86_64-linux-gnu/libncurses.so.5.9 +lib/x86_64-linux-gnu/libnsl-2.19.so +lib/x86_64-linux-gnu/libnsl.so.1 +lib/x86_64-linux-gnu/libnss_compat-2.19.so +lib/x86_64-linux-gnu/libnss_compat.so.2 +lib/x86_64-linux-gnu/libnss_dns-2.19.so +lib/x86_64-linux-gnu/libnss_dns.so.2 +lib/x86_64-linux-gnu/libnss_files-2.19.so +lib/x86_64-linux-gnu/libnss_files.so.2 +lib/x86_64-linux-gnu/libnss_nis-2.19.so +lib/x86_64-linux-gnu/libnss_nis.so.2 +lib/x86_64-linux-gnu/libpam.so.0 +lib/x86_64-linux-gnu/libpam.so.0.83.1 +lib/x86_64-linux-gnu/libpcre.so.3 +lib/x86_64-linux-gnu/libpcre.so.3.13.3 +lib/x86_64-linux-gnu/libpng12.so.0 +lib/x86_64-linux-gnu/libpng12.so.0.50.0 +lib/x86_64-linux-gnu/libpthread-2.19.so +lib/x86_64-linux-gnu/libpthread.so.0 +lib/x86_64-linux-gnu/libreadline.so.6 +lib/x86_64-linux-gnu/libreadline.so.6.3 +lib/x86_64-linux-gnu/libresolv-2.19.so +lib/x86_64-linux-gnu/libresolv.so.2 +lib/x86_64-linux-gnu/librt-2.19.so +lib/x86_64-linux-gnu/librt.so.1 +lib/x86_64-linux-gnu/libselinux.so.1 +lib/x86_64-linux-gnu/libsystemd.so.0 +lib/x86_64-linux-gnu/libsystemd.so.0.3.1 +lib/x86_64-linux-gnu/libtinfo.so.5 +lib/x86_64-linux-gnu/libtinfo.so.5.9 +lib/x86_64-linux-gnu/libutil-2.19.so +lib/x86_64-linux-gnu/libutil.so.1 +lib/x86_64-linux-gnu/libwrap.so.0 +lib/x86_64-linux-gnu/libwrap.so.0.7.6 +lib/x86_64-linux-gnu/libz.so.1 +lib/x86_64-linux-gnu/libz.so.1.2.8 +lib64/ld-linux-x86-64.so.2 +opt/app +opt/app/.env +opt/app/.sandstorm/launcher.sh +opt/app/.sandstorm/service-config/mime.types +opt/app/.sandstorm/service-config/nginx.conf +opt/app/app/Bootstrap/ConfigureLogging.php +opt/app/app/Console/Commands/CreateImport.php +opt/app/app/Console/Commands/EncryptFile.php +opt/app/app/Console/Commands/Import.php +opt/app/app/Console/Commands/ScanAttachments.php +opt/app/app/Console/Commands/UpgradeDatabase.php +opt/app/app/Console/Commands/UpgradeFireflyInstructions.php +opt/app/app/Console/Commands/UseEncryption.php +opt/app/app/Console/Commands/VerifyDatabase.php +opt/app/app/Console/Kernel.php +opt/app/app/Exceptions/Handler.php +opt/app/app/Generator/Chart/Basic/ChartJsGenerator.php +opt/app/app/Generator/Chart/Basic/GeneratorInterface.php +opt/app/app/Helpers/Attachments/AttachmentHelper.php +opt/app/app/Helpers/Attachments/AttachmentHelperInterface.php +opt/app/app/Helpers/Collector/JournalCollector.php +opt/app/app/Helpers/Collector/JournalCollectorInterface.php +opt/app/app/Helpers/FiscalHelper.php +opt/app/app/Helpers/FiscalHelperInterface.php +opt/app/app/Helpers/Report/ReportHelper.php +opt/app/app/Helpers/Report/ReportHelperInterface.php +opt/app/app/Http/Controllers/Auth/LoginController.php +opt/app/app/Http/Controllers/BudgetController.php +opt/app/app/Http/Controllers/Chart/AccountController.php +opt/app/app/Http/Controllers/Chart/BudgetController.php +opt/app/app/Http/Controllers/Chart/CategoryController.php +opt/app/app/Http/Controllers/Controller.php +opt/app/app/Http/Controllers/HomeController.php +opt/app/app/Http/Controllers/JavascriptController.php +opt/app/app/Http/Controllers/JsonController.php +opt/app/app/Http/Controllers/NewUserController.php +opt/app/app/Http/Controllers/ProfileController.php +opt/app/app/Http/Controllers/ReportController.php +opt/app/app/Http/Controllers/Transaction/SingleController.php +opt/app/app/Http/Kernel.php +opt/app/app/Http/Middleware/Authenticate.php +opt/app/app/Http/Middleware/AuthenticateTwoFactor.php +opt/app/app/Http/Middleware/Binder.php +opt/app/app/Http/Middleware/EncryptCookies.php +opt/app/app/Http/Middleware/Range.php +opt/app/app/Http/Middleware/RedirectIfAuthenticated.php +opt/app/app/Http/Middleware/Sandstorm.php +opt/app/app/Http/Middleware/StartFireflySession.php +opt/app/app/Http/Middleware/VerifyCsrfToken.php +opt/app/app/Http/Requests/JournalFormRequest.php +opt/app/app/Http/Requests/NewUserFormRequest.php +opt/app/app/Http/Requests/ProfileFormRequest.php +opt/app/app/Http/Requests/Request.php +opt/app/app/Http/breadcrumbs.php +opt/app/app/Jobs/Job.php +opt/app/app/Jobs/MailError.php +opt/app/app/Models/Account.php +opt/app/app/Models/AccountMeta.php +opt/app/app/Models/AccountType.php +opt/app/app/Models/AvailableBudget.php +opt/app/app/Models/Bill.php +opt/app/app/Models/Budget.php +opt/app/app/Models/BudgetLimit.php +opt/app/app/Models/Category.php +opt/app/app/Models/Configuration.php +opt/app/app/Models/PiggyBank.php +opt/app/app/Models/Preference.php +opt/app/app/Models/Role.php +opt/app/app/Models/Tag.php +opt/app/app/Models/Transaction.php +opt/app/app/Models/TransactionCurrency.php +opt/app/app/Models/TransactionJournal.php +opt/app/app/Models/TransactionType.php +opt/app/app/Providers/AccountServiceProvider.php +opt/app/app/Providers/AppServiceProvider.php +opt/app/app/Providers/AttachmentServiceProvider.php +opt/app/app/Providers/AuthServiceProvider.php +opt/app/app/Providers/BillServiceProvider.php +opt/app/app/Providers/BudgetServiceProvider.php +opt/app/app/Providers/CategoryServiceProvider.php +opt/app/app/Providers/CurrencyServiceProvider.php +opt/app/app/Providers/EventServiceProvider.php +opt/app/app/Providers/ExportJobServiceProvider.php +opt/app/app/Providers/FireflyServiceProvider.php +opt/app/app/Providers/FireflySessionProvider.php +opt/app/app/Providers/JournalServiceProvider.php +opt/app/app/Providers/LogServiceProvider.php +opt/app/app/Providers/PiggyBankServiceProvider.php +opt/app/app/Providers/RouteServiceProvider.php +opt/app/app/Providers/RuleGroupServiceProvider.php +opt/app/app/Providers/RuleServiceProvider.php +opt/app/app/Providers/SearchServiceProvider.php +opt/app/app/Providers/TagServiceProvider.php +opt/app/app/Repositories/Account/AccountRepository.php +opt/app/app/Repositories/Account/AccountRepositoryInterface.php +opt/app/app/Repositories/Account/AccountTasker.php +opt/app/app/Repositories/Account/AccountTaskerInterface.php +opt/app/app/Repositories/Bill/BillRepository.php +opt/app/app/Repositories/Bill/BillRepositoryInterface.php +opt/app/app/Repositories/Budget/BudgetRepository.php +opt/app/app/Repositories/Budget/BudgetRepositoryInterface.php +opt/app/app/Repositories/Category/CategoryRepository.php +opt/app/app/Repositories/Category/CategoryRepositoryInterface.php +opt/app/app/Repositories/Journal/JournalRepository.php +opt/app/app/Repositories/Journal/JournalRepositoryInterface.php +opt/app/app/Repositories/PiggyBank/PiggyBankRepository.php +opt/app/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php +opt/app/app/Repositories/Tag/TagRepository.php +opt/app/app/Repositories/Tag/TagRepositoryInterface.php +opt/app/app/Repositories/User/UserRepositoryInterface.php +opt/app/app/Support/Amount.php +opt/app/app/Support/CacheProperties.php +opt/app/app/Support/Domain.php +opt/app/app/Support/ExpandedForm.php +opt/app/app/Support/Facades/Amount.php +opt/app/app/Support/Facades/ExpandedForm.php +opt/app/app/Support/Facades/FireflyConfig.php +opt/app/app/Support/Facades/Navigation.php +opt/app/app/Support/Facades/Preferences.php +opt/app/app/Support/Facades/Steam.php +opt/app/app/Support/FireflyConfig.php +opt/app/app/Support/Models/TagSupport.php +opt/app/app/Support/Models/TransactionJournalSupport.php +opt/app/app/Support/Navigation.php +opt/app/app/Support/Preferences.php +opt/app/app/Support/Steam.php +opt/app/app/Support/Twig/General.php +opt/app/app/Support/Twig/Journal.php +opt/app/app/Support/Twig/PiggyBank.php +opt/app/app/Support/Twig/Rule.php +opt/app/app/Support/Twig/Transaction.php +opt/app/app/Support/Twig/Translation.php +opt/app/app/User.php +opt/app/app/Validation/FireflyValidator.php +opt/app/artisan +opt/app/bootstrap/app.php +opt/app/bootstrap/autoload.php +opt/app/bootstrap/cache/services.php +opt/app/config +opt/app/config/app.php +opt/app/config/auth.php +opt/app/config/broadcasting.php +opt/app/config/cache.php +opt/app/config/compile.php +opt/app/config/csv.php +opt/app/config/database.php +opt/app/config/filesystems.php +opt/app/config/firefly.php +opt/app/config/mail.php +opt/app/config/queue.php +opt/app/config/services.php +opt/app/config/session.php +opt/app/config/twigbridge.php +opt/app/config/upgrade.php +opt/app/config/view.php +opt/app/database/migrations +opt/app/database/migrations/2016_06_16_000000_create_support_tables.php +opt/app/database/migrations/2016_06_16_000001_create_users_table.php +opt/app/database/migrations/2016_06_16_000002_create_main_tables.php +opt/app/database/migrations/2016_08_25_091522_changes_for_3101.php +opt/app/database/migrations/2016_09_12_121359_fix_nullables.php +opt/app/database/migrations/2016_10_09_150037_expand_transactions_table.php +opt/app/database/migrations/2016_10_22_075804_changes_for_v410.php +opt/app/database/migrations/2016_11_24_210552_changes_for_v420.php +opt/app/database/migrations/2016_12_22_150431_changes_for_v430.php +opt/app/database/migrations/2016_12_28_203205_changes_for_v431.php +opt/app/database/seeds/AccountTypeSeeder.php +opt/app/database/seeds/DatabaseSeeder.php +opt/app/database/seeds/PermissionSeeder.php +opt/app/database/seeds/TransactionCurrencySeeder.php +opt/app/database/seeds/TransactionTypeSeeder.php +opt/app/public/css/bootstrap-multiselect.css +opt/app/public/css/bootstrap-tagsinput.css +opt/app/public/css/bootstrap-tour.min.css +opt/app/public/css/daterangepicker.css +opt/app/public/css/firefly.css +opt/app/public/css/jquery-ui/jquery-ui.structure.min.css +opt/app/public/css/jquery-ui/jquery-ui.theme.min.css +opt/app/public/index.php +opt/app/public/js/ff/budgets/index.js +opt/app/public/js/ff/charts.defaults.js +opt/app/public/js/ff/charts.js +opt/app/public/js/ff/firefly.js +opt/app/public/js/ff/guest.js +opt/app/public/js/ff/help.js +opt/app/public/js/ff/index.js +opt/app/public/js/ff/reports/index.js +opt/app/public/js/ff/transactions/single/create.js +opt/app/public/js/lib/Chart.bundle.min.js +opt/app/public/js/lib/accounting.min.js +opt/app/public/js/lib/bootstrap-multiselect.js +opt/app/public/js/lib/bootstrap-tagsinput.min.js +opt/app/public/js/lib/bootstrap-tour.min.js +opt/app/public/js/lib/bootstrap3-typeahead.min.js +opt/app/public/js/lib/daterangepicker.js +opt/app/public/js/lib/jquery-3.1.1.min.js +opt/app/public/js/lib/jquery-ui.min.js +opt/app/public/js/lib/modernizr-custom.js +opt/app/public/js/lib/moment.min.js +opt/app/public/lib/adminlte/css/AdminLTE.min.css +opt/app/public/lib/adminlte/css/skins/skin-blue-light.min.css +opt/app/public/lib/adminlte/js/app.min.js +opt/app/public/lib/bootstrap/css/bootstrap.min.css +opt/app/public/lib/bootstrap/js/bootstrap.min.js +opt/app/public/lib/font-awesome/css/font-awesome.min.css +opt/app/public/lib/font-awesome/fonts/fontawesome-webfont.woff2 +opt/app/resources/lang/en_US/breadcrumbs.php +opt/app/resources/lang/en_US/config.php +opt/app/resources/lang/en_US/firefly.php +opt/app/resources/lang/en_US/form.php +opt/app/resources/lang/en_US/help.php +opt/app/resources/lang/en_US/validation.php +opt/app/resources/views/auth/login.twig +opt/app/resources/views/budgets/index.twig +opt/app/resources/views/emails/error-html.twig +opt/app/resources/views/emails/error-text.twig +opt/app/resources/views/emails/footer-html.twig +opt/app/resources/views/emails/footer-text.twig +opt/app/resources/views/emails/header-html.twig +opt/app/resources/views/emails/header-text.twig +opt/app/resources/views/error.twig +opt/app/resources/views/form/amount.twig +opt/app/resources/views/form/balance.twig +opt/app/resources/views/form/date.twig +opt/app/resources/views/form/feedback.twig +opt/app/resources/views/form/help.twig +opt/app/resources/views/form/options.twig +opt/app/resources/views/form/select.twig +opt/app/resources/views/form/text.twig +opt/app/resources/views/index.twig +opt/app/resources/views/javascript/variables.twig +opt/app/resources/views/json/tour.twig +opt/app/resources/views/layout/default.twig +opt/app/resources/views/layout/guest.twig +opt/app/resources/views/list/journals-tiny-tasker.twig +opt/app/resources/views/new-user/index.twig +opt/app/resources/views/partials/boxes.twig +opt/app/resources/views/partials/control-bar.twig +opt/app/resources/views/partials/favicons.twig +opt/app/resources/views/partials/flashes.twig +opt/app/resources/views/partials/menu-sidebar.twig +opt/app/resources/views/partials/page-header.twig +opt/app/resources/views/profile/change-password.twig +opt/app/resources/views/profile/delete-account.twig +opt/app/resources/views/profile/index.twig +opt/app/resources/views/reports/index.twig +opt/app/resources/views/reports/options/no-options.twig +opt/app/resources/views/transactions/single/create.twig +opt/app/routes/api.php +opt/app/routes/console.php +opt/app/routes/web.php +opt/app/storage +opt/app/vendor/autoload.php +opt/app/vendor/composer/ClassLoader.php +opt/app/vendor/composer/autoload_real.php +opt/app/vendor/composer/autoload_static.php +opt/app/vendor/davejamesmiller/laravel-breadcrumbs/config/breadcrumbs.php +opt/app/vendor/davejamesmiller/laravel-breadcrumbs/src/CurrentRoute.php +opt/app/vendor/davejamesmiller/laravel-breadcrumbs/src/Facade.php +opt/app/vendor/davejamesmiller/laravel-breadcrumbs/src/Generator.php +opt/app/vendor/davejamesmiller/laravel-breadcrumbs/src/Manager.php +opt/app/vendor/davejamesmiller/laravel-breadcrumbs/src/ServiceProvider.php +opt/app/vendor/davejamesmiller/laravel-breadcrumbs/src/View.php +opt/app/vendor/davejamesmiller/laravel-breadcrumbs/views/bootstrap3.blade.php +opt/app/vendor/doctrine/common/lib/Doctrine/Common/EventManager.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Configuration.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Connection.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/DriverException.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/ExceptionConverterDriver.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOException.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOMySql/Driver.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/ResultStatement.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/ServerInfoAwareConnection.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Statement.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Events.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/KeywordList.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQLKeywords.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Query/Expression/ExpressionBuilder.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractAsset.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Column.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Constraint.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/ForeignKeyConstraint.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Identifier.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Index.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Table.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/ArrayType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/BigIntType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/BinaryType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/BlobType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/BooleanType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeTzType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DateType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DecimalType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/FloatType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/GuidType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/IntegerType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/JsonArrayType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/ObjectType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/SimpleArrayType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/SmallIntType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/StringType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/TextType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/TimeType.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Type.php +opt/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/VersionAwarePlatformDriver.php +opt/app/vendor/doctrine/inflector/lib/Doctrine/Common/Inflector/Inflector.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/Access/Gate.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/Access/HandlesAuthorization.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/AuthManager.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/AuthServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/Authenticatable.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/Console/ClearResetsCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/Console/MakeAuthCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/CreatesUserProviders.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/EloquentUserProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/Events/Authenticated.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/Events/Login.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/GuardHelpers.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/Passwords/CanResetPassword.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/Passwords/PasswordResetServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php +opt/app/vendor/laravel/framework/src/Illuminate/Broadcasting/BroadcastServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Bus/BusServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php +opt/app/vendor/laravel/framework/src/Illuminate/Bus/Queueable.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/CacheManager.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/CacheServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/Console/CacheTableCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/Console/ClearCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/Console/ForgetCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/Events/CacheEvent.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/Events/CacheHit.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/Events/CacheMissed.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/Events/KeyForgotten.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/Events/KeyWritten.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/FileStore.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/Repository.php +opt/app/vendor/laravel/framework/src/Illuminate/Cache/RetrievesMultipleKeys.php +opt/app/vendor/laravel/framework/src/Illuminate/Config/Repository.php +opt/app/vendor/laravel/framework/src/Illuminate/Console/Application.php +opt/app/vendor/laravel/framework/src/Illuminate/Console/Command.php +opt/app/vendor/laravel/framework/src/Illuminate/Console/ConfirmableTrait.php +opt/app/vendor/laravel/framework/src/Illuminate/Console/DetectsApplicationNamespace.php +opt/app/vendor/laravel/framework/src/Illuminate/Console/Events/ArtisanStarting.php +opt/app/vendor/laravel/framework/src/Illuminate/Console/GeneratorCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Console/OutputStyle.php +opt/app/vendor/laravel/framework/src/Illuminate/Console/Parser.php +opt/app/vendor/laravel/framework/src/Illuminate/Console/Scheduling/Schedule.php +opt/app/vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleFinishCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php +opt/app/vendor/laravel/framework/src/Illuminate/Container/Container.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Auth/Access/Authorizable.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Auth/Access/Gate.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Auth/Authenticatable.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Auth/CanResetPassword.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Auth/Factory.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Auth/Guard.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Auth/StatefulGuard.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Auth/SupportsBasicAuth.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Auth/UserProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Bus/Dispatcher.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Bus/QueueingDispatcher.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Cache/Factory.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Cache/Repository.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Cache/Store.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Config/Repository.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Console/Application.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Console/Kernel.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Container/Container.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Cookie/Factory.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Cookie/QueueingFactory.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Debug/ExceptionHandler.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Encryption/DecryptException.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Encryption/Encrypter.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Events/Dispatcher.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Filesystem/Cloud.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Filesystem/Factory.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Filesystem/FileNotFoundException.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Filesystem/Filesystem.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Foundation/Application.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Hashing/Hasher.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Http/Kernel.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Logging/Log.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Mail/MailQueue.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Mail/Mailer.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Pagination/Paginator.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Pipeline/Pipeline.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Queue/Factory.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Queue/Job.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Queue/Monitor.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Queue/Queue.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Queue/QueueableCollection.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Queue/QueueableEntity.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Queue/ShouldQueue.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Routing/BindingRegistrar.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Routing/Registrar.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Routing/ResponseFactory.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Routing/UrlGenerator.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Routing/UrlRoutable.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Session/Session.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Support/Arrayable.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Support/Htmlable.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Support/Jsonable.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Support/MessageBag.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Support/MessageProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Support/Renderable.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Translation/Translator.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Validation/Factory.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Validation/ValidatesWhenResolved.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Validation/Validator.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/View/Factory.php +opt/app/vendor/laravel/framework/src/Illuminate/Contracts/View/View.php +opt/app/vendor/laravel/framework/src/Illuminate/Cookie/CookieJar.php +opt/app/vendor/laravel/framework/src/Illuminate/Cookie/CookieServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php +opt/app/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/ConnectionInterface.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/ConnectionResolverInterface.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Connectors/ConnectionFactory.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Connectors/ConnectorInterface.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Connectors/MySqlConnector.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/BaseCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/InstallCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/MigrateCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/RefreshCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/ResetCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/RollbackCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/StatusCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Console/Seeds/SeedCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Console/Seeds/SeederMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/DatabaseManager.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/DatabaseServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/DetectsDeadlocks.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/DetectsLostConnections.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Collection.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/HasMany.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/Relation.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Scope.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/SoftDeletes.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/SoftDeletingScope.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Events/QueryExecuted.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Events/StatementPrepared.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Grammar.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/MigrationServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migration.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Migrations/MigrationCreator.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/MySqlConnection.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Query/Expression.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Query/Grammars/Grammar.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Query/JoinClause.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Query/Processors/MySqlProcessor.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Query/Processors/Processor.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/QueryException.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Schema/Grammars/ChangeColumn.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Schema/Grammars/Grammar.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Schema/Grammars/RenameColumn.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Schema/MySqlBuilder.php +opt/app/vendor/laravel/framework/src/Illuminate/Database/Seeder.php +opt/app/vendor/laravel/framework/src/Illuminate/Encryption/Encrypter.php +opt/app/vendor/laravel/framework/src/Illuminate/Encryption/EncryptionServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php +opt/app/vendor/laravel/framework/src/Illuminate/Events/EventServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php +opt/app/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php +opt/app/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemManager.php +opt/app/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/AliasLoader.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Application.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Auth/Access/Authorizable.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticatesUsers.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Auth/RedirectsUsers.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Auth/ThrottlesLogins.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Auth/User.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/BootProviders.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/RegisterFacades.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/SetRequestForConsole.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Bus/DispatchesJobs.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/AppNameCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/ClearCompiledCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/ConfigCacheCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/ConfigClearCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/ConsoleMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/DownCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/EnvironmentCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/EventGenerateCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/EventMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/JobMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/KeyGenerateCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/ListenerMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/MailMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/ModelMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/NotificationMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/OptimizeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/PolicyMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/ProviderMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/RequestMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/RouteCacheCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/RouteClearCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/RouteListCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/ServeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/StorageLinkCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/TestMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/UpCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/VendorPublishCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/ViewClearCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/EnvironmentDetector.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Events/LocaleUpdated.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Http/Events/RequestHandled.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Http/FormRequest.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/ProviderRepository.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Providers/ComposerServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Providers/ConsoleSupportServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Providers/FormRequestServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Support/Providers/AuthServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Validation/ValidatesRequests.php +opt/app/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php +opt/app/vendor/laravel/framework/src/Illuminate/Hashing/BcryptHasher.php +opt/app/vendor/laravel/framework/src/Illuminate/Hashing/HashServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php +opt/app/vendor/laravel/framework/src/Illuminate/Http/Concerns/InteractsWithFlashData.php +opt/app/vendor/laravel/framework/src/Illuminate/Http/Concerns/InteractsWithInput.php +opt/app/vendor/laravel/framework/src/Illuminate/Http/JsonResponse.php +opt/app/vendor/laravel/framework/src/Illuminate/Http/RedirectResponse.php +opt/app/vendor/laravel/framework/src/Illuminate/Http/Request.php +opt/app/vendor/laravel/framework/src/Illuminate/Http/Response.php +opt/app/vendor/laravel/framework/src/Illuminate/Http/ResponseTrait.php +opt/app/vendor/laravel/framework/src/Illuminate/Log/Events/MessageLogged.php +opt/app/vendor/laravel/framework/src/Illuminate/Log/LogServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Log/Writer.php +opt/app/vendor/laravel/framework/src/Illuminate/Mail/Events/MessageSending.php +opt/app/vendor/laravel/framework/src/Illuminate/Mail/MailServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Mail/Mailer.php +opt/app/vendor/laravel/framework/src/Illuminate/Mail/Message.php +opt/app/vendor/laravel/framework/src/Illuminate/Mail/TransportManager.php +opt/app/vendor/laravel/framework/src/Illuminate/Notifications/Console/NotificationTableCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Notifications/HasDatabaseNotifications.php +opt/app/vendor/laravel/framework/src/Illuminate/Notifications/Notifiable.php +opt/app/vendor/laravel/framework/src/Illuminate/Notifications/NotificationServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Notifications/RoutesNotifications.php +opt/app/vendor/laravel/framework/src/Illuminate/Pagination/AbstractPaginator.php +opt/app/vendor/laravel/framework/src/Illuminate/Pagination/PaginationServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Pagination/Paginator.php +opt/app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php +opt/app/vendor/laravel/framework/src/Illuminate/Pipeline/PipelineServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Connectors/ConnectorInterface.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Connectors/SyncConnector.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Console/FailedTableCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Console/FlushFailedCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Console/ForgetFailedCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Console/ListFailedCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Console/ListenCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Console/RestartCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Console/RetryCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Console/TableCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Events/JobProcessed.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Events/JobProcessing.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/InteractsWithQueue.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Jobs/SyncJob.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Listener.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Queue.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/QueueManager.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/QueueServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/SerializesModels.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/SyncQueue.php +opt/app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php +opt/app/vendor/laravel/framework/src/Illuminate/Redis/RedisServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Console/ControllerMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Console/MiddlewareMakeCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Controller.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/ControllerMiddlewareOptions.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Events/RouteMatched.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/ImplicitRouteBinding.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Matching/HostValidator.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Matching/MethodValidator.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Matching/SchemeValidator.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Matching/UriValidator.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Matching/ValidatorInterface.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/MiddlewareNameResolver.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Redirector.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/ResponseFactory.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Route.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/RouteAction.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/RouteCompiler.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/RouteDependencyResolverTrait.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/RouteGroup.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/RouteParameterBinder.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/RouteSignatureParameters.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/RouteUrlGenerator.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/Router.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/RoutingServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/SortedMiddleware.php +opt/app/vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php +opt/app/vendor/laravel/framework/src/Illuminate/Session/Console/SessionTableCommand.php +opt/app/vendor/laravel/framework/src/Illuminate/Session/EncryptedStore.php +opt/app/vendor/laravel/framework/src/Illuminate/Session/FileSessionHandler.php +opt/app/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php +opt/app/vendor/laravel/framework/src/Illuminate/Session/SessionManager.php +opt/app/vendor/laravel/framework/src/Illuminate/Session/SessionServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Session/Store.php +opt/app/vendor/laravel/framework/src/Illuminate/Session/TokenMismatchException.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/AggregateServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Arr.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Collection.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Composer.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/App.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Auth.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Cache.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Config.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Crypt.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/DB.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Event.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Gate.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Input.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Log.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Mail.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Request.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Response.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Route.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Schema.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Session.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Storage.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/URL.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/Validator.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Facades/View.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Fluent.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/HtmlString.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Manager.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/MessageBag.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/NamespacedItemResolver.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Pluralizer.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/ServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Str.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/Traits/Macroable.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/ViewErrorBag.php +opt/app/vendor/laravel/framework/src/Illuminate/Support/helpers.php +opt/app/vendor/laravel/framework/src/Illuminate/Translation/FileLoader.php +opt/app/vendor/laravel/framework/src/Illuminate/Translation/LoaderInterface.php +opt/app/vendor/laravel/framework/src/Illuminate/Translation/TranslationServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Translation/Translator.php +opt/app/vendor/laravel/framework/src/Illuminate/Validation/Concerns/FormatsMessages.php +opt/app/vendor/laravel/framework/src/Illuminate/Validation/Concerns/ReplacesAttributes.php +opt/app/vendor/laravel/framework/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +opt/app/vendor/laravel/framework/src/Illuminate/Validation/DatabasePresenceVerifier.php +opt/app/vendor/laravel/framework/src/Illuminate/Validation/Factory.php +opt/app/vendor/laravel/framework/src/Illuminate/Validation/PresenceVerifierInterface.php +opt/app/vendor/laravel/framework/src/Illuminate/Validation/ValidatesWhenResolvedTrait.php +opt/app/vendor/laravel/framework/src/Illuminate/Validation/ValidationException.php +opt/app/vendor/laravel/framework/src/Illuminate/Validation/ValidationRuleParser.php +opt/app/vendor/laravel/framework/src/Illuminate/Validation/ValidationServiceProvider.php +opt/app/vendor/laravel/framework/src/Illuminate/Validation/Validator.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/BladeCompiler.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Compiler.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/CompilerInterface.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesAuthorizations.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesComments.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesEchos.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesIncludes.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesInjections.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesLayouts.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesLoops.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesRawPhp.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesStacks.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesTranslations.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Concerns/ManagesComponents.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Concerns/ManagesEvents.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Concerns/ManagesLayouts.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Concerns/ManagesLoops.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Concerns/ManagesStacks.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Concerns/ManagesTranslations.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Engines/CompilerEngine.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Engines/EngineInterface.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Engines/EngineResolver.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Engines/PhpEngine.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Factory.php +opt/app/vendor/laravel/framework/src/Illuminate/View/FileViewFinder.php +opt/app/vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.php +opt/app/vendor/laravel/framework/src/Illuminate/View/View.php +opt/app/vendor/laravel/framework/src/Illuminate/View/ViewFinderInterface.php +opt/app/vendor/laravel/framework/src/Illuminate/View/ViewName.php +opt/app/vendor/laravel/framework/src/Illuminate/View/ViewServiceProvider.php +opt/app/vendor/laravelcollective/html/src/Componentable.php +opt/app/vendor/laravelcollective/html/src/FormBuilder.php +opt/app/vendor/laravelcollective/html/src/FormFacade.php +opt/app/vendor/laravelcollective/html/src/HtmlBuilder.php +opt/app/vendor/laravelcollective/html/src/HtmlServiceProvider.php +opt/app/vendor/laravelcollective/html/src/helpers.php +opt/app/vendor/league/flysystem/src/Adapter/AbstractAdapter.php +opt/app/vendor/league/flysystem/src/Adapter/Local.php +opt/app/vendor/league/flysystem/src/AdapterInterface.php +opt/app/vendor/league/flysystem/src/Config.php +opt/app/vendor/league/flysystem/src/ConfigAwareTrait.php +opt/app/vendor/league/flysystem/src/Filesystem.php +opt/app/vendor/league/flysystem/src/FilesystemInterface.php +opt/app/vendor/league/flysystem/src/Plugin/PluggableTrait.php +opt/app/vendor/league/flysystem/src/ReadInterface.php +opt/app/vendor/monolog/monolog/src/Monolog/Formatter/FormatterInterface.php +opt/app/vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php +opt/app/vendor/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php +opt/app/vendor/monolog/monolog/src/Monolog/Handler/AbstractHandler.php +opt/app/vendor/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php +opt/app/vendor/monolog/monolog/src/Monolog/Handler/AbstractSyslogHandler.php +opt/app/vendor/monolog/monolog/src/Monolog/Handler/HandlerInterface.php +opt/app/vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php +opt/app/vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php +opt/app/vendor/monolog/monolog/src/Monolog/Handler/SyslogHandler.php +opt/app/vendor/monolog/monolog/src/Monolog/Logger.php +opt/app/vendor/nesbot/carbon/src/Carbon/Carbon.php +opt/app/vendor/nesbot/carbon/src/Carbon/Lang/en.php +opt/app/vendor/paragonie/random_compat/lib/random.php +opt/app/vendor/pragmarx/google2fa/src/Vendor/Laravel/ServiceProvider.php +opt/app/vendor/psr/log/Psr/Log/LoggerInterface.php +opt/app/vendor/rcrowe/twigbridge/config/twigbridge.php +opt/app/vendor/rcrowe/twigbridge/src/Bridge.php +opt/app/vendor/rcrowe/twigbridge/src/Command/Clean.php +opt/app/vendor/rcrowe/twigbridge/src/Command/Lint.php +opt/app/vendor/rcrowe/twigbridge/src/Command/TwigBridge.php +opt/app/vendor/rcrowe/twigbridge/src/Engine/Compiler.php +opt/app/vendor/rcrowe/twigbridge/src/Engine/Twig.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Laravel/Auth.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Laravel/Config.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Laravel/Dump.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Laravel/Input.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Laravel/Session.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Laravel/Str.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Laravel/Translator.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Laravel/Url.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Loader/Facade/Caller.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Loader/Facades.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Loader/Filters.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Loader/Functions.php +opt/app/vendor/rcrowe/twigbridge/src/Extension/Loader/Loader.php +opt/app/vendor/rcrowe/twigbridge/src/Facade/Twig.php +opt/app/vendor/rcrowe/twigbridge/src/ServiceProvider.php +opt/app/vendor/rcrowe/twigbridge/src/Twig/Globals.php +opt/app/vendor/rcrowe/twigbridge/src/Twig/Loader.php +opt/app/vendor/rcrowe/twigbridge/src/Twig/Template.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/AbstractFilterableInputStream.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterReaderFactory.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterReaderFactory/SimpleCharacterReaderFactory.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterStream.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/CharacterStream/NgCharacterStream.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/DependencyContainer.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Encoder.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Encoder/QpEncoder.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Encoder/Rfc2231Encoder.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/Event.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/EventDispatcher.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/EventObject.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/SimpleEventDispatcher.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/TransportChangeEvent.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Events/TransportExceptionEvent.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Filterable.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/InputByteStream.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/IoException.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/KeyCacheInputStream.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/SimpleKeyCacheInputStream.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mailer.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Message.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/CharsetObserver.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/NativeQpContentEncoder.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/QpContentEncoder.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/QpContentEncoderProxy.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/EncodingObserver.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Grammar.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Header.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/HeaderEncoder.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/HeaderEncoder/QpHeaderEncoder.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/HeaderFactory.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/HeaderSet.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/AbstractHeader.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/DateHeader.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/IdentificationHeader.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/MailboxHeader.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/ParameterizedHeader.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/UnstructuredHeader.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Message.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/MimeEntity.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/MimePart.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ParameterizedHeader.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleHeaderFactory.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleHeaderSet.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleMessage.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleMimeEntity.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/MimePart.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/OutputByteStream.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Preferences.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ReplacementFilterFactory.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/SmtpTransport.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/StreamFilter.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/StreamFilters/ByteArrayReplacementFilter.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/StreamFilters/StringReplacementFilterFactory.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/SwiftException.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/AbstractSmtpTransport.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Auth/CramMd5Authenticator.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Auth/LoginAuthenticator.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Auth/NTLMAuthenticator.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Auth/PlainAuthenticator.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Auth/XOAuth2Authenticator.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/AuthHandler.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Authenticator.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/EsmtpHandler.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/EsmtpTransport.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/IoBuffer.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/SmtpAgent.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php +opt/app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/TransportException.php +opt/app/vendor/swiftmailer/swiftmailer/lib/dependency_maps/cache_deps.php +opt/app/vendor/swiftmailer/swiftmailer/lib/dependency_maps/message_deps.php +opt/app/vendor/swiftmailer/swiftmailer/lib/dependency_maps/mime_deps.php +opt/app/vendor/swiftmailer/swiftmailer/lib/dependency_maps/transport_deps.php +opt/app/vendor/swiftmailer/swiftmailer/lib/mime_types.php +opt/app/vendor/swiftmailer/swiftmailer/lib/preferences.php +opt/app/vendor/swiftmailer/swiftmailer/lib/swift_init.php +opt/app/vendor/swiftmailer/swiftmailer/lib/swift_required.php +opt/app/vendor/symfony/console/Application.php +opt/app/vendor/symfony/console/Command/Command.php +opt/app/vendor/symfony/console/Command/HelpCommand.php +opt/app/vendor/symfony/console/Command/ListCommand.php +opt/app/vendor/symfony/console/Formatter/OutputFormatter.php +opt/app/vendor/symfony/console/Formatter/OutputFormatterInterface.php +opt/app/vendor/symfony/console/Formatter/OutputFormatterStyle.php +opt/app/vendor/symfony/console/Formatter/OutputFormatterStyleInterface.php +opt/app/vendor/symfony/console/Formatter/OutputFormatterStyleStack.php +opt/app/vendor/symfony/console/Helper/DebugFormatterHelper.php +opt/app/vendor/symfony/console/Helper/FormatterHelper.php +opt/app/vendor/symfony/console/Helper/Helper.php +opt/app/vendor/symfony/console/Helper/HelperInterface.php +opt/app/vendor/symfony/console/Helper/HelperSet.php +opt/app/vendor/symfony/console/Helper/ProcessHelper.php +opt/app/vendor/symfony/console/Helper/QuestionHelper.php +opt/app/vendor/symfony/console/Input/ArgvInput.php +opt/app/vendor/symfony/console/Input/ArrayInput.php +opt/app/vendor/symfony/console/Input/Input.php +opt/app/vendor/symfony/console/Input/InputArgument.php +opt/app/vendor/symfony/console/Input/InputDefinition.php +opt/app/vendor/symfony/console/Input/InputInterface.php +opt/app/vendor/symfony/console/Input/InputOption.php +opt/app/vendor/symfony/console/Input/StreamableInputInterface.php +opt/app/vendor/symfony/console/Output/BufferedOutput.php +opt/app/vendor/symfony/console/Output/ConsoleOutput.php +opt/app/vendor/symfony/console/Output/ConsoleOutputInterface.php +opt/app/vendor/symfony/console/Output/Output.php +opt/app/vendor/symfony/console/Output/OutputInterface.php +opt/app/vendor/symfony/console/Output/StreamOutput.php +opt/app/vendor/symfony/console/Style/OutputStyle.php +opt/app/vendor/symfony/console/Style/StyleInterface.php +opt/app/vendor/symfony/console/Style/SymfonyStyle.php +opt/app/vendor/symfony/console/Terminal.php +opt/app/vendor/symfony/debug/Exception/FatalErrorException.php +opt/app/vendor/symfony/debug/Exception/FatalThrowableError.php +opt/app/vendor/symfony/debug/Exception/FlattenException.php +opt/app/vendor/symfony/debug/ExceptionHandler.php +opt/app/vendor/symfony/finder/Comparator/Comparator.php +opt/app/vendor/symfony/finder/Comparator/DateComparator.php +opt/app/vendor/symfony/finder/Finder.php +opt/app/vendor/symfony/finder/Glob.php +opt/app/vendor/symfony/finder/Iterator/DateRangeFilterIterator.php +opt/app/vendor/symfony/finder/Iterator/ExcludeDirectoryFilterIterator.php +opt/app/vendor/symfony/finder/Iterator/FileTypeFilterIterator.php +opt/app/vendor/symfony/finder/Iterator/FilenameFilterIterator.php +opt/app/vendor/symfony/finder/Iterator/FilterIterator.php +opt/app/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php +opt/app/vendor/symfony/finder/Iterator/PathFilterIterator.php +opt/app/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php +opt/app/vendor/symfony/finder/SplFileInfo.php +opt/app/vendor/symfony/http-foundation/AcceptHeader.php +opt/app/vendor/symfony/http-foundation/AcceptHeaderItem.php +opt/app/vendor/symfony/http-foundation/Cookie.php +opt/app/vendor/symfony/http-foundation/FileBag.php +opt/app/vendor/symfony/http-foundation/HeaderBag.php +opt/app/vendor/symfony/http-foundation/JsonResponse.php +opt/app/vendor/symfony/http-foundation/ParameterBag.php +opt/app/vendor/symfony/http-foundation/RedirectResponse.php +opt/app/vendor/symfony/http-foundation/Request.php +opt/app/vendor/symfony/http-foundation/Response.php +opt/app/vendor/symfony/http-foundation/ResponseHeaderBag.php +opt/app/vendor/symfony/http-foundation/ServerBag.php +opt/app/vendor/symfony/http-foundation/Session/SessionBagInterface.php +opt/app/vendor/symfony/http-foundation/Session/SessionInterface.php +opt/app/vendor/symfony/http-foundation/Session/Storage/MetadataBag.php +opt/app/vendor/symfony/http-kernel/HttpKernelInterface.php +opt/app/vendor/symfony/polyfill-mbstring/bootstrap.php +opt/app/vendor/symfony/polyfill-php56/bootstrap.php +opt/app/vendor/symfony/process/ExecutableFinder.php +opt/app/vendor/symfony/process/PhpExecutableFinder.php +opt/app/vendor/symfony/process/ProcessUtils.php +opt/app/vendor/symfony/routing/CompiledRoute.php +opt/app/vendor/symfony/routing/Route.php +opt/app/vendor/symfony/routing/RouteCompiler.php +opt/app/vendor/symfony/routing/RouteCompilerInterface.php +opt/app/vendor/symfony/translation/Loader/ArrayLoader.php +opt/app/vendor/symfony/translation/Loader/LoaderInterface.php +opt/app/vendor/symfony/translation/MessageSelector.php +opt/app/vendor/symfony/translation/Translator.php +opt/app/vendor/symfony/translation/TranslatorBagInterface.php +opt/app/vendor/symfony/translation/TranslatorInterface.php +opt/app/vendor/symfony/var-dumper/Cloner/AbstractCloner.php +opt/app/vendor/symfony/var-dumper/Cloner/ClonerInterface.php +opt/app/vendor/symfony/var-dumper/Cloner/VarCloner.php +opt/app/vendor/symfony/var-dumper/Resources/functions/dump.php +opt/app/vendor/twig/twig/lib/Twig/BaseNodeVisitor.php +opt/app/vendor/twig/twig/lib/Twig/Cache/Filesystem.php +opt/app/vendor/twig/twig/lib/Twig/CacheInterface.php +opt/app/vendor/twig/twig/lib/Twig/Compiler.php +opt/app/vendor/twig/twig/lib/Twig/CompilerInterface.php +opt/app/vendor/twig/twig/lib/Twig/Environment.php +opt/app/vendor/twig/twig/lib/Twig/ExistsLoaderInterface.php +opt/app/vendor/twig/twig/lib/Twig/ExpressionParser.php +opt/app/vendor/twig/twig/lib/Twig/Extension.php +opt/app/vendor/twig/twig/lib/Twig/Extension/Core.php +opt/app/vendor/twig/twig/lib/Twig/Extension/Debug.php +opt/app/vendor/twig/twig/lib/Twig/Extension/Escaper.php +opt/app/vendor/twig/twig/lib/Twig/Extension/GlobalsInterface.php +opt/app/vendor/twig/twig/lib/Twig/Extension/Optimizer.php +opt/app/vendor/twig/twig/lib/Twig/Extension/Staging.php +opt/app/vendor/twig/twig/lib/Twig/ExtensionInterface.php +opt/app/vendor/twig/twig/lib/Twig/Lexer.php +opt/app/vendor/twig/twig/lib/Twig/LexerInterface.php +opt/app/vendor/twig/twig/lib/Twig/Loader/Array.php +opt/app/vendor/twig/twig/lib/Twig/Loader/Chain.php +opt/app/vendor/twig/twig/lib/Twig/LoaderInterface.php +opt/app/vendor/twig/twig/lib/Twig/Markup.php +opt/app/vendor/twig/twig/lib/Twig/Node.php +opt/app/vendor/twig/twig/lib/Twig/Node/Block.php +opt/app/vendor/twig/twig/lib/Twig/Node/BlockReference.php +opt/app/vendor/twig/twig/lib/Twig/Node/Body.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Array.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/AssignName.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Binary.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Binary/And.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Concat.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Equal.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Greater.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mod.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mul.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotEqual.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Or.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Call.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Conditional.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Constant.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Filter.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Filter/Default.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Function.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/GetAttr.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Name.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Test.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Test/Defined.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Unary.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Unary/Neg.php +opt/app/vendor/twig/twig/lib/Twig/Node/Expression/Unary/Not.php +opt/app/vendor/twig/twig/lib/Twig/Node/For.php +opt/app/vendor/twig/twig/lib/Twig/Node/ForLoop.php +opt/app/vendor/twig/twig/lib/Twig/Node/If.php +opt/app/vendor/twig/twig/lib/Twig/Node/Include.php +opt/app/vendor/twig/twig/lib/Twig/Node/Module.php +opt/app/vendor/twig/twig/lib/Twig/Node/Print.php +opt/app/vendor/twig/twig/lib/Twig/Node/Set.php +opt/app/vendor/twig/twig/lib/Twig/Node/Text.php +opt/app/vendor/twig/twig/lib/Twig/NodeInterface.php +opt/app/vendor/twig/twig/lib/Twig/NodeOutputInterface.php +opt/app/vendor/twig/twig/lib/Twig/NodeTraverser.php +opt/app/vendor/twig/twig/lib/Twig/NodeVisitor/Escaper.php +opt/app/vendor/twig/twig/lib/Twig/NodeVisitor/Optimizer.php +opt/app/vendor/twig/twig/lib/Twig/NodeVisitor/SafeAnalysis.php +opt/app/vendor/twig/twig/lib/Twig/NodeVisitorInterface.php +opt/app/vendor/twig/twig/lib/Twig/Parser.php +opt/app/vendor/twig/twig/lib/Twig/ParserInterface.php +opt/app/vendor/twig/twig/lib/Twig/SimpleFilter.php +opt/app/vendor/twig/twig/lib/Twig/SimpleFunction.php +opt/app/vendor/twig/twig/lib/Twig/SimpleTest.php +opt/app/vendor/twig/twig/lib/Twig/Source.php +opt/app/vendor/twig/twig/lib/Twig/SourceContextLoaderInterface.php +opt/app/vendor/twig/twig/lib/Twig/Template.php +opt/app/vendor/twig/twig/lib/Twig/TemplateInterface.php +opt/app/vendor/twig/twig/lib/Twig/Token.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/AutoEscape.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Block.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Do.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Embed.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Extends.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Filter.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Flush.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/For.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/From.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/If.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Import.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Include.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Macro.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Set.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Spaceless.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/Use.php +opt/app/vendor/twig/twig/lib/Twig/TokenParser/With.php +opt/app/vendor/twig/twig/lib/Twig/TokenParserBroker.php +opt/app/vendor/twig/twig/lib/Twig/TokenParserBrokerInterface.php +opt/app/vendor/twig/twig/lib/Twig/TokenParserInterface.php +opt/app/vendor/twig/twig/lib/Twig/TokenStream.php +opt/app/vendor/vlucas/phpdotenv/src/Dotenv.php +opt/app/vendor/vlucas/phpdotenv/src/Loader.php +opt/app/vendor/watson/validating/src/Injectors/UniqueInjector.php +opt/app/vendor/watson/validating/src/ValidatingObserver.php +opt/app/vendor/watson/validating/src/ValidatingTrait.php +proc/cpuinfo +sandstorm-http-bridge +sandstorm-http-bridge-config +sandstorm-manifest +usr/bin/my_print_defaults +usr/bin/mysql +usr/bin/mysql_install_db +usr/bin/php +usr/bin/php7.0 +usr/bin/sudo +usr/lib/locale/locale-archive +usr/lib/php/20151012/bcmath.so +usr/lib/php/20151012/calendar.so +usr/lib/php/20151012/ctype.so +usr/lib/php/20151012/curl.so +usr/lib/php/20151012/dom.so +usr/lib/php/20151012/exif.so +usr/lib/php/20151012/fileinfo.so +usr/lib/php/20151012/ftp.so +usr/lib/php/20151012/gettext.so +usr/lib/php/20151012/iconv.so +usr/lib/php/20151012/intl.so +usr/lib/php/20151012/json.so +usr/lib/php/20151012/mbstring.so +usr/lib/php/20151012/mysqli.so +usr/lib/php/20151012/mysqlnd.so +usr/lib/php/20151012/opcache.so +usr/lib/php/20151012/pdo.so +usr/lib/php/20151012/pdo_mysql.so +usr/lib/php/20151012/phar.so +usr/lib/php/20151012/posix.so +usr/lib/php/20151012/readline.so +usr/lib/php/20151012/shmop.so +usr/lib/php/20151012/simplexml.so +usr/lib/php/20151012/sockets.so +usr/lib/php/20151012/sysvmsg.so +usr/lib/php/20151012/sysvsem.so +usr/lib/php/20151012/sysvshm.so +usr/lib/php/20151012/tokenizer.so +usr/lib/php/20151012/wddx.so +usr/lib/php/20151012/xml.so +usr/lib/php/20151012/xmlreader.so +usr/lib/php/20151012/xmlwriter.so +usr/lib/php/20151012/xsl.so +usr/lib/ssl/openssl.cnf +usr/lib/x86_64-linux-gnu/libGeoIP.so.1 +usr/lib/x86_64-linux-gnu/libGeoIP.so.1.6.2 +usr/lib/x86_64-linux-gnu/libX11.so.6 +usr/lib/x86_64-linux-gnu/libX11.so.6.3.0 +usr/lib/x86_64-linux-gnu/libXau.so.6 +usr/lib/x86_64-linux-gnu/libXau.so.6.0.0 +usr/lib/x86_64-linux-gnu/libXdmcp.so.6 +usr/lib/x86_64-linux-gnu/libXdmcp.so.6.0.0 +usr/lib/x86_64-linux-gnu/libXpm.so.4 +usr/lib/x86_64-linux-gnu/libXpm.so.4.11.0 +usr/lib/x86_64-linux-gnu/libapparmor.so.1 +usr/lib/x86_64-linux-gnu/libapparmor.so.1.2.0 +usr/lib/x86_64-linux-gnu/libcrypto.so.1.0.0 +usr/lib/x86_64-linux-gnu/libcurl.so.4 +usr/lib/x86_64-linux-gnu/libcurl.so.4.3.0 +usr/lib/x86_64-linux-gnu/libdb-5.3.so +usr/lib/x86_64-linux-gnu/libedit.so.2 +usr/lib/x86_64-linux-gnu/libedit.so.2.0.51 +usr/lib/x86_64-linux-gnu/libexslt.so.0 +usr/lib/x86_64-linux-gnu/libexslt.so.0.8.17 +usr/lib/x86_64-linux-gnu/libffi.so.6 +usr/lib/x86_64-linux-gnu/libffi.so.6.0.2 +usr/lib/x86_64-linux-gnu/libfontconfig.so.1 +usr/lib/x86_64-linux-gnu/libfontconfig.so.1.8.0 +usr/lib/x86_64-linux-gnu/libfreetype.so.6 +usr/lib/x86_64-linux-gnu/libfreetype.so.6.11.1 +usr/lib/x86_64-linux-gnu/libgd.so.3 +usr/lib/x86_64-linux-gnu/libgd.so.3.0.0 +usr/lib/x86_64-linux-gnu/libgmp.so.10 +usr/lib/x86_64-linux-gnu/libgmp.so.10.2.0 +usr/lib/x86_64-linux-gnu/libgnutls-deb0.so.28 +usr/lib/x86_64-linux-gnu/libgnutls-deb0.so.28.41.0 +usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2 +usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2.2 +usr/lib/x86_64-linux-gnu/libhogweed.so.2 +usr/lib/x86_64-linux-gnu/libhogweed.so.2.5 +usr/lib/x86_64-linux-gnu/libicudata.so.52 +usr/lib/x86_64-linux-gnu/libicudata.so.52.1 +usr/lib/x86_64-linux-gnu/libicui18n.so.52 +usr/lib/x86_64-linux-gnu/libicui18n.so.52.1 +usr/lib/x86_64-linux-gnu/libicuio.so.52 +usr/lib/x86_64-linux-gnu/libicuio.so.52.1 +usr/lib/x86_64-linux-gnu/libicuuc.so.52 +usr/lib/x86_64-linux-gnu/libicuuc.so.52.1 +usr/lib/x86_64-linux-gnu/libidn.so.11 +usr/lib/x86_64-linux-gnu/libidn.so.11.6.12 +usr/lib/x86_64-linux-gnu/libjbig.so.0 +usr/lib/x86_64-linux-gnu/libjpeg.so.62 +usr/lib/x86_64-linux-gnu/libjpeg.so.62.1.0 +usr/lib/x86_64-linux-gnu/libk5crypto.so.3 +usr/lib/x86_64-linux-gnu/libk5crypto.so.3.1 +usr/lib/x86_64-linux-gnu/libkrb5.so.3 +usr/lib/x86_64-linux-gnu/libkrb5.so.3.3 +usr/lib/x86_64-linux-gnu/libkrb5support.so.0 +usr/lib/x86_64-linux-gnu/libkrb5support.so.0.1 +usr/lib/x86_64-linux-gnu/liblber-2.4.so.2 +usr/lib/x86_64-linux-gnu/liblber-2.4.so.2.10.3 +usr/lib/x86_64-linux-gnu/libldap_r-2.4.so.2 +usr/lib/x86_64-linux-gnu/libldap_r-2.4.so.2.10.3 +usr/lib/x86_64-linux-gnu/libmysqlclient.so.18 +usr/lib/x86_64-linux-gnu/libmysqlclient.so.18.0.0 +usr/lib/x86_64-linux-gnu/libnettle.so.4 +usr/lib/x86_64-linux-gnu/libnettle.so.4.7 +usr/lib/x86_64-linux-gnu/libossp-uuid.so.16 +usr/lib/x86_64-linux-gnu/libossp-uuid.so.16.0.22 +usr/lib/x86_64-linux-gnu/libp11-kit.so.0 +usr/lib/x86_64-linux-gnu/libp11-kit.so.0.0.0 +usr/lib/x86_64-linux-gnu/librtmp.so.1 +usr/lib/x86_64-linux-gnu/libsasl2.so.2 +usr/lib/x86_64-linux-gnu/libsasl2.so.2.0.25 +usr/lib/x86_64-linux-gnu/libssh2.so.1 +usr/lib/x86_64-linux-gnu/libssh2.so.1.0.1 +usr/lib/x86_64-linux-gnu/libssl.so.1.0.0 +usr/lib/x86_64-linux-gnu/libstdc++.so.6 +usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.20 +usr/lib/x86_64-linux-gnu/libtasn1.so.6 +usr/lib/x86_64-linux-gnu/libtasn1.so.6.3.2 +usr/lib/x86_64-linux-gnu/libtiff.so.5 +usr/lib/x86_64-linux-gnu/libtiff.so.5.2.0 +usr/lib/x86_64-linux-gnu/libvpx.so.1 +usr/lib/x86_64-linux-gnu/libvpx.so.1.3.0 +usr/lib/x86_64-linux-gnu/libxcb.so.1 +usr/lib/x86_64-linux-gnu/libxcb.so.1.1.0 +usr/lib/x86_64-linux-gnu/libxml2.so.2 +usr/lib/x86_64-linux-gnu/libxml2.so.2.9.1 +usr/lib/x86_64-linux-gnu/libxslt.so.1 +usr/lib/x86_64-linux-gnu/libxslt.so.1.1.28 +usr/sbin/mysqld +usr/sbin/nginx +usr/sbin/php-fpm7.0 +usr/share/locale/locale.alias +usr/share/mysql/charsets/Index.xml +usr/share/mysql/english/errmsg.sys +usr/share/mysql/fill_help_tables.sql +usr/share/mysql/mysql_system_tables.sql +usr/share/mysql/mysql_system_tables_data.sql diff --git a/.sandstorm/sandstorm-pkgdef.capnp b/.sandstorm/sandstorm-pkgdef.capnp new file mode 100644 index 0000000000..c0493e5fb6 --- /dev/null +++ b/.sandstorm/sandstorm-pkgdef.capnp @@ -0,0 +1,188 @@ +@0x9411e6c8b3c8a4b6; + +using Spk = import "/sandstorm/package.capnp"; +# This imports: +# $SANDSTORM_HOME/latest/usr/include/sandstorm/package.capnp +# Check out that file to see the full, documented package definition format. + +const pkgdef :Spk.PackageDefinition = ( + # The package definition. Note that the spk tool looks specifically for the + # "pkgdef" constant. + + id = "uws252ya9mep4t77tevn85333xzsgrpgth8q4y1rhknn1hammw70", + # Your app ID is actually its public key. The private key was placed in + # your keyring. All updates must be signed with the same key. + + manifest = ( + appTitle = (defaultText = "Firefly III"), + appVersion = 1, + appMarketingVersion = (defaultText = "3.4.3"), + actions = [ + # Define your "new document" handlers here. + ( nounPhrase = (defaultText = "administration"), + command = .myCommand + # The command to run when starting for the first time. (".myCommand" + # is just a constant defined at the bottom of the file.) + ) + ], + + continueCommand = .myCommand, + # This is the command called to start your app back up after it has been + # shut down for inactivity. Here we're using the same command as for + # starting a new instance, but you could use different commands for each + # case. + + metadata = ( + icons = ( + appGrid = (png = (dpi1x = embed "app-graphics/firefly-iii-128.png")), + grain = (png = (dpi1x = embed "app-graphics/firefly-iii-24.png", + dpi2x = embed "app-graphics/firefly-iii-48.png")), + market = (png = (dpi1x = embed "app-graphics/firefly-iii-150.png")) + ), + + website = "https://firefly-iii.github.io/", + codeUrl = "https://github.com/firefly-iii/firefly-iii", + #license = (openSource = mit), + license = (proprietary = (defaultText = embed "../LICENSE")), + # The license this package is distributed under. See + # https://docs.sandstorm.io/en/latest/developing/publishing-apps/#license + + categories = [productivity], + # A list of categories/genres to which this app belongs, sorted with best fit first. + # See the list of categories at + # https://docs.sandstorm.io/en/latest/developing/publishing-apps/#categories + + author = ( + contactEmail = "thegrumpydictator@gmail.com", + upstreamAuthor = "James Cole", + pgpSignature = embed "pgp-signature", + ), + + pgpKeyring = embed "pgp-keyring", + description = (defaultText = embed "description.md"), + shortDescription = (defaultText = "Financial management"), + screenshots = [ + # Screenshots to use for marketing purposes. Examples below. + # Sizes are given in device-independent pixels, so if you took these + # screenshots on a Retina-style high DPI screen, divide each dimension by two. + + (width = 1200, height = 1000, png = embed "screenshots/screenshot-1.png"), + (width = 1200, height = 1000, png = embed "screenshots/screenshot-2.png"), + (width = 1200, height = 1518, png = embed "screenshots/screenshot-3.png"), + + ], + changeLog = (defaultText = embed "changelog.md"), + ), + ), + + sourceMap = ( + # Here we defined where to look for files to copy into your package. The + # `spk dev` command actually figures out what files your app needs + # automatically by running it on a FUSE filesystem. So, the mappings + # here are only to tell it where to find files that the app wants. + searchPath = [ + ( sourcePath = "." ), # Search this directory first. + ( sourcePath = "/", # Then search the system root directory. + hidePaths = [ "home", "proc", "sys", + "etc/passwd", "etc/hosts", "etc/host.conf", + "etc/nsswitch.conf", "etc/resolv.conf" ] + # You probably don't want the app pulling files from these places, + # so we hide them. Note that /dev, /var, and /tmp are implicitly + # hidden because Sandstorm itself provides them. + ) + ] + ), + + fileList = "sandstorm-files.list", + # `spk dev` will write a list of all the files your app uses to this file. + # You should review it later, before shipping your app. + + alwaysInclude = [], + # Fill this list with more names of files or directories that should be + # included in your package, even if not listed in sandstorm-files.list. + # Use this to force-include stuff that you know you need but which may + # not have been detected as a dependency during `spk dev`. If you list + # a directory here, its entire contents will be included recursively. + + #bridgeConfig = ( + # # Used for integrating permissions and roles into the Sandstorm shell + # # and for sandstorm-http-bridge to pass to your app. + # # Uncomment this block and adjust the permissions and roles to make + # # sense for your app. + # # For more information, see high-level documentation at + # # https://docs.sandstorm.io/en/latest/developing/auth/ + # # and advanced details in the "BridgeConfig" section of + # # https://github.com/sandstorm-io/sandstorm/blob/master/src/sandstorm/package.capnp + # viewInfo = ( + # # For details on the viewInfo field, consult "ViewInfo" in + # # https://github.com/sandstorm-io/sandstorm/blob/master/src/sandstorm/grain.capnp + # + # permissions = [ + # # Permissions which a user may or may not possess. A user's current + # # permissions are passed to the app as a comma-separated list of `name` + # # fields in the X-Sandstorm-Permissions header with each request. + # # + # # IMPORTANT: only ever append to this list! Reordering or removing fields + # # will change behavior and permissions for existing grains! To deprecate a + # # permission, or for more information, see "PermissionDef" in + # # https://github.com/sandstorm-io/sandstorm/blob/master/src/sandstorm/grain.capnp + # ( + # name = "editor", + # # Name of the permission, used as an identifier for the permission in cases where string + # # names are preferred. Used in sandstorm-http-bridge's X-Sandstorm-Permissions HTTP header. + # + # title = (defaultText = "editor"), + # # Display name of the permission, e.g. to display in a checklist of permissions + # # that may be assigned when sharing. + # + # description = (defaultText = "grants ability to modify data"), + # # Prose describing what this role means, suitable for a tool tip or similar help text. + # ), + # ], + # roles = [ + # # Roles are logical collections of permissions. For instance, your app may have + # # a "viewer" role and an "editor" role + # ( + # title = (defaultText = "editor"), + # # Name of the role. Shown in the Sandstorm UI to indicate which users have which roles. + # + # permissions = [true], + # # An array indicating which permissions this role carries. + # # It should be the same length as the permissions array in + # # viewInfo, and the order of the lists must match. + # + # verbPhrase = (defaultText = "can make changes to the document"), + # # Brief explanatory text to show in the sharing UI indicating + # # what a user assigned this role will be able to do with the grain. + # + # description = (defaultText = "editors may view all site data and change settings."), + # # Prose describing what this role means, suitable for a tool tip or similar help text. + # ), + # ( + # title = (defaultText = "viewer"), + # permissions = [false], + # verbPhrase = (defaultText = "can view the document"), + # description = (defaultText = "viewers may view what other users have written."), + # ), + # ], + # ), + # #apiPath = "/api", + # # Apps can export an API to the world. The API is to be used primarily by Javascript + # # code and native apps, so it can't serve out regular HTML to browsers. If a request + # # comes in to your app's API, sandstorm-http-bridge will prefix the request's path with + # # this string, if specified. + #), +); + +const myCommand :Spk.Manifest.Command = ( + # Here we define the command used to start up your server. + argv = ["/sandstorm-http-bridge", "8000", "--", "/opt/app/.sandstorm/launcher.sh"], + environ = [ + # Note that this defines the *entire* environment seen by your app. + (key = "PATH", value = "/usr/local/bin:/usr/bin:/bin"), + (key = "SANDSTORM", value = "1"), + # Export SANDSTORM=1 into the environment, so that apps running within Sandstorm + # can detect if $SANDSTORM="1" at runtime, switching UI and/or backend to use + # the app's Sandstorm-specific integration code. + ] +); diff --git a/.sandstorm/screenshots/screenshot-1.png b/.sandstorm/screenshots/screenshot-1.png new file mode 100644 index 0000000000..f72ceb48e7 Binary files /dev/null and b/.sandstorm/screenshots/screenshot-1.png differ diff --git a/.sandstorm/screenshots/screenshot-2.png b/.sandstorm/screenshots/screenshot-2.png new file mode 100644 index 0000000000..9301851850 Binary files /dev/null and b/.sandstorm/screenshots/screenshot-2.png differ diff --git a/.sandstorm/screenshots/screenshot-3.png b/.sandstorm/screenshots/screenshot-3.png new file mode 100644 index 0000000000..20fcab548f Binary files /dev/null and b/.sandstorm/screenshots/screenshot-3.png differ diff --git a/.sandstorm/service-config/mime.types b/.sandstorm/service-config/mime.types new file mode 100644 index 0000000000..89be9a4cd6 --- /dev/null +++ b/.sandstorm/service-config/mime.types @@ -0,0 +1,89 @@ + +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + image/svg+xml svg svgz; + image/webp webp; + + application/font-woff woff; + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.wap.wmlc wmlc; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + application/vnd.openxmlformats-officedocument.wordprocessingml.document docx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx; + application/vnd.openxmlformats-officedocument.presentationml.presentation pptx; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/.sandstorm/service-config/nginx.conf b/.sandstorm/service-config/nginx.conf new file mode 100644 index 0000000000..b63ddaae07 --- /dev/null +++ b/.sandstorm/service-config/nginx.conf @@ -0,0 +1,87 @@ +worker_processes 4; +pid /var/run/nginx.pid; + +events { + worker_connections 768; + # multi_accept on; +} + +http { + # Basic Settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + # server_names_hash_bucket_size 64; + server_tokens off; + server_name_in_redirect off; + + include mime.types; + default_type application/octet-stream; + + # Logging + access_log off; + error_log stderr; + + # Prevent nginx from adding compression; this interacts badly with Sandstorm + # WebSession due to https://github.com/sandstorm-io/sandstorm/issues/289 + gzip off; + + # Trust the sandstorm-http-bridge's X-Forwarded-Proto. + map $http_x_forwarded_proto $fe_https { + default ""; + https on; + } + + server { + listen 8000 default_server; + listen [::]:8000 default_server ipv6only=on; + + # Allow arbitrarily large bodies - Sandstorm can handle them, and requests + # are authenticated already, so there's no reason for apps to add additional + # limits by default. + client_max_body_size 0; + + server_name localhost; + root /opt/app/public; + location / { + index index.php; + try_files $uri $uri/ /index.php?$query_string; + autoindex on; + sendfile off; + } + location ~ \.php$ { + try_files $uri =404; + fastcgi_pass unix:/var/run/php7.0-fpm.sock; + fastcgi_index index.php; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + + + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param DOCUMENT_URI $document_uri; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param HTTPS $fe_https if_not_empty; + + fastcgi_param GATEWAY_INTERFACE CGI/1.1; + fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + + fastcgi_param REMOTE_ADDR $remote_addr; + fastcgi_param REMOTE_PORT $remote_port; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_NAME $server_name; + + # PHP only, required if PHP was built with --enable-force-cgi-redirect + #fastcgi_param REDIRECT_STATUS 200; + } + } +} diff --git a/.sandstorm/setup.sh b/.sandstorm/setup.sh new file mode 100755 index 0000000000..9af16630f0 --- /dev/null +++ b/.sandstorm/setup.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# When you change this file, you must take manual action. Read this doc: +# - https://docs.sandstorm.io/en/latest/vagrant-spk/customizing/#setupsh + +set -euo pipefail + +export DEBIAN_FRONTEND=noninteractive + +# install packages so we can install apt-add-repository. +apt-get update +apt-get install -y python-software-properties software-properties-common + +# actually add repository +apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E9C74FEEA2098A6E +add-apt-repository "deb http://packages.dotdeb.org jessie all" + +# install packages. +apt-get update +apt-get install -y nginx php7.0-fpm php7.0-mysql php7.0-cli php7.0-curl git php7.0-dev php7.0-intl php7.0-dom php7.0-mbstring php7.0-bcmath mysql-server +service nginx stop +service php7.0-fpm stop +service mysql stop +systemctl disable nginx +systemctl disable php7.0-fpm +systemctl disable mysql +# patch /etc/php/7.0/fpm/pool.d/www.conf to not change uid/gid to www-data +sed --in-place='' \ + --expression='s/^listen.owner = www-data/;listen.owner = www-data/' \ + --expression='s/^listen.group = www-data/;listen.group = www-data/' \ + /etc/php/7.0/fpm/pool.d/www.conf +# patch /etc/php/7.0/fpm/php-fpm.conf to not have a pidfile +sed --in-place='' \ + --expression='s/^pid =/;pid =/' \ + /etc/php/7.0/fpm/php-fpm.conf + +# move sock file to better dir: +sed --in-place='' \ + --expression='s/^listen = \/run\/php\/php7.0-fpm.sock/listen = \/var\/run\/php7.0-fpm.sock/' \ + /etc/php/7.0/fpm/pool.d/www.conf + +# patch /etc/php/7.0/fpm/pool.d/www.conf to no clear environment variables +# so we can pass in SANDSTORM=1 to apps +sed --in-place='' \ + --expression='s/^;clear_env = no/clear_env=no/' \ + /etc/php/7.0/fpm/pool.d/www.conf +# patch mysql conf to not change uid, and to use /var/tmp over /tmp +# for secure-file-priv see https://github.com/sandstorm-io/vagrant-spk/issues/195 +sed --in-place='' \ + --expression='s/^user\t\t= mysql/#user\t\t= mysql/' \ + --expression='s,^tmpdir\t\t= /tmp,tmpdir\t\t= /var/tmp,' \ + --expression='/\[mysqld]/ a\ secure-file-priv = ""\' \ + /etc/mysql/my.cnf +# patch mysql conf to use smaller transaction logs to save disk space +cat < /etc/mysql/conf.d/sandstorm.cnf +[mysqld] +# Set the transaction log file to the minimum allowed size to save disk space. +# innodb_log_file_size = 1048576 +# Set the main data file to grow by 1MB at a time, rather than 8MB at a time. +innodb_autoextend_increment = 1 +EOF diff --git a/.sandstorm/stack b/.sandstorm/stack new file mode 100644 index 0000000000..79a9408192 --- /dev/null +++ b/.sandstorm/stack @@ -0,0 +1 @@ +lemp diff --git a/.scrutinizer.yml b/.scrutinizer.yml index b2245b4a46..ac754cc957 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -2,7 +2,50 @@ tools: external_code_coverage: false filter: - excluded_paths: - - app/Support/Migration/* - - app/database/migrations/* - - database/migrations/* + paths: + - app/* + - public/js/ff/* + excluded_paths: + - "database/migrations/*" + - "bootstrap/*" + - "config/*" + - "docker/*" + - "public/js/lib/*" + - "public/lib/adminlte/js/*" + - "public/lib/bootstrap/js/*" + - "resources/*" + - "routes/*" + - "storage/*" +checks: + php: + use_self_instead_of_fqcn: true + uppercase_constants: true + return_doc_comments: true + return_doc_comment_if_not_inferrable: true + remove_extra_empty_lines: true + parameter_doc_comments: true + optional_parameters_at_the_end: true + no_short_variable_names: + minimum: '3' + no_short_method_names: + minimum: '3' + no_long_variable_names: + maximum: '20' + no_goto: true + newline_at_end_of_file: true + encourage_single_quotes: true + avoid_todo_comments: true + avoid_perl_style_comments: true + avoid_fixme_comments: true + avoid_multiple_statements_on_same_line: true + align_assignments: true + duplication: false + javascript: true + +coding_style: + php: + spaces: + around_operators: + concatenation: true + other: + after_type_cast: false \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..040654a669 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: php +php: + - 7.0 + - 7.1 + +cache: + directories: + - vendor + - $HOME/.composer/cache + +install: + - if [[ "$(php -v | grep 'PHP 7')" ]]; then phpenv config-rm xdebug.ini; fi + - rm composer.lock + - composer update --no-scripts + - cp .env.testing .env + - php artisan clear-compiled + - php artisan optimize + - php artisan env + - cp .env.testing .env + - mv storage/database/databasecopy.sqlite storage/database/database.sqlite + +script: + - phpunit \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9181780696..73c018a7db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,254 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [4.3.5] - 2017-02-19 +### Added +- Beta support for Sandstorm.IO +- Docker support by [@schoentoon](https://github.com/schoentoon), [@elohmeier](https://github.com/elohmeier), [@patrickkostjens](https://github.com/patrickkostjens) and [@crash7](https://github.com/crash7)! + +### Changed +- Updated to laravel 5.4! +- User friendly error message +- Updated locales to support more operating systems, first reported in #536 by [dabenzel](https://github.com/dabenzel) +- Updated budget report +- Improved 404 page +- Smooth curves, improved by [elamperti](https://github.com/elamperti). + +### Fixed +- #549 +- #553 +- Fixed #559 reported by [elamperti](https://github.com/elamperti). +- #565, as reported by a user over the mail +- #566, as reported by [dspeckmann](https://github.com/dspeckmann) +- #567, as reported by [winsomniak](https://github.com/winsomniak) +- #569, as reported by [winsomniak](https://github.com/winsomniak) +- #572, as reported by [zjean](https://github.com/zjean) +- Many issues with the transaction filters which will fix reports (they tended to display the wrong amount). + +## [4.3.4] - 2017-02-02 +### Fixed +- Fixed bug #550, reported by [worldworm](https://github.com/worldworm)! +- Fixed bug #551, reported by [t-me](https://github.com/t-me)! + +## [4.3.3] - 2017-01-30 + +_The 100th release of Firefly!_ + +### Added +- Add locales to Docker (#534) by [elohmeier](https://github.com/elohmeier). +- Optional database encryption. On by default. +- Datepicker for Firefox and other browsers. +- New instruction block for updating and installing. +- Ability to clone transactions. +- Use multi-select Bootstrap thing instead of massive lists of checkboxes. + +### Removed +- Lots of old Javascript + +### Fixed +- Missing sort broke various charts +- Bug in reports that made amounts behave weird +- Various bug fixes + +### Security +- Tested FF against the naughty string list. + +## [4.3.2] - 2017-01-09 + +An intermediate release because something in the Twig and Twigbridge libraries is broken and I have to make sure it doesn't affect you guys. But some cool features were on their way so there's that oo. + +### Added +- Some code for issue #475, consistent overviews. +- Better currency display. Make sure you have locale packages installed. + +### Changed +- Uses a new version of Laravel. + +### Fixed +- The password reset routine was broken. +- Issue #522, thanks to [xpfgsyb](https://github.com/xpfgsyb) +- Issue #524, thanks to [worldworm](https://github.com/worldworm) +- Issue #526, thanks to [worldworm](https://github.com/worldworm) +- Issue #528, thanks to [skibbipl](https://github.com/skibbipl) +- Various other fixes. + +## [4.3.1] - 2017-01-04 +### Added +- Support for Russian and Polish. +- Support for a proper demo website. +- Support for custom decimal places in currencies (#506, suggested by [xpfgsyb](https://github.com/xpfgsyb)). +- Most amounts are now right-aligned (#511, suggested by [xpfgsyb](https://github.com/xpfgsyb)). +- German is now a "complete" language, more than 75% translated! + +### Changed +- **[New Github repository!](github.com/firefly-iii/firefly-iii)** +- Better category overview. +- #502, thanks to [zjean](https://github.com/zjean) + +### Removed +- Removed a lot of administration functions. +- Removed ability to activate users. + +### Fixed +- #501, thanks to [zjean](https://github.com/zjean) +- #513, thanks to [skibbipl](https://github.com/skibbipl) + +### Security +- #519, thanks to [xpfgsyb](https://github.com/xpfgsyb) + +## [4.3.0] - 2015-12-26 +### Added +- New method of keeping track of available budget, see issue #489 +- Support for Spanish +- Firefly III now has an extended demo mode. Will expand further in the future. + + +### Changed +- New favicon +- Import routine no longer gives transactions a description #483 + + +### Removed +- All test data generation code. + +### Fixed +- Removed import accounts from search results #478 +- Redirect after delete will no longer go back to deleted item #477 +- Cannot math #482 +- Fixed bug in virtual balance field #479 + +## [4.2.2] - 2016-12-18 +### Added +- New budget report (still a bit of a beta) +- Can now edit user + +### Changed +- New config for specific events. Still need to build Notifications. + +### Fixed +- Various bugs +- Issue #472 thanks to [zjean](https://github.com/zjean) + +## [4.2.1] - 2016-12-09 +### Added +- BIC support (see #430) +- New category report section and chart (see the general financial report) + + +### Changed +- Date range picker now also available on mobile devices (see #435) +- Extended range of amounts for issue #439 +- Rewrote all routes. Old bookmarks may break. + +## [4.2.0] - 2016-11-27 +### Added +- Lots of (empty) tests +- Expanded transaction lists (#377) +- New charts at account view +- First code for #305 + + +### Changed +- Updated all email messages. +- Made some fonts local + + +### Deprecated +- Initial release. + +### Removed +- Initial release. + +### Fixed +- Issue #408 +- Various issues with split journals +- Issue #414, thx [zjean](https://github.com/zjean) +- Issue #419, thx [schwalberich](https://github.com/schwalberich) +- Issue #422, thx [xzaz](https://github.com/xzaz) +- Various import bugs, such as #416 ([zjean](https://github.com/zjean)) + + +### Security +- Initial release. + + +## [4.1.7] - 2016-11-19 +### Added +- Check for database table presence in console commands. +- Category report +- Reinstated old test routines. + + +### Changed +- Confirm account setting is no longer in `.env` file. +- Titles are now in reverse (current page > parent > firefly iii) +- Easier update of language files thanks to Github implementation. +- Uniform colours for charts. + +### Fixed +- Made all pages more mobile friendly. +- Fixed #395 found by [marcoveeneman](https://github.com/marcoveeneman). +- Fixed #398 found by [marcoveeneman](https://github.com/marcoveeneman). +- Fixed #401 found by [marcoveeneman](https://github.com/marcoveeneman). +- Many optimizations. +- Updated many libraries. +- Various bugs found by myself. + + +## [4.1.6] - 2016-11-06 +### Added +- New budget table for multi year report. + +### Changed +- Greatly expanded help pages and their function. +- Built a new transaction collector, which I think was the idea of [roberthorlings](https://github.com/roberthorlings) originally. +- Rebuilt seach engine. + +### Fixed +- #375, thanks to [schoentoon](https://github.com/schoentoon) which made it impossible to resurrect currencies. +- #370 thanks to [ksmolder](https://github.com/ksmolder) +- #378, thanks to [HomelessAvatar](https://github.com/HomelessAvatar) + +## [4.1.5] - 2016-11-01 +### Changed +- Report parts are loaded using AJAX, making a lot of code more simple. +- Help content will fall back to English. +- Help content is translated through Crowdin. + +### Fixed +- Issue #370 + +## [4.1.4] - 2016-10-30 +### Added +- New Dockerfile thanks to [schoentoon](https://github.com/schoentoon) +- Added changing the destination account as rule action. +- Added changing the source account as rule action. +- Can convert transactions into different types. + +### Changed +- Changed the export routine to be more future-proof. +- Improved help routine. +- Integrated CrowdIn translations. +- Simplified reports +- Change error message to refer to solution. + +### Fixed +- #367 thanks to [HungryFeline](https://github.com/HungryFeline) +- #366 thanks to [3mz3t](https://github.com/3mz3t) +- #362 and #341 thanks to [bnw](https://github.com/bnw) +- #355 thanks to [roberthorlings](https://github.com/roberthorlings) + +## [4.1.3] - 2016-10-22 +### Fixed +- Some event handlers called the wrong method. + +## [4.1.2] - 2016-10-22 + +### Fixed +- A bug is fixed in the journal event handler that prevented Firefly III from actually storing journals. + ## [4.1.1] - 2016-10-22 + ### Added - Option to show deposit accounts on the front page. - Script to upgrade split transactions @@ -22,29 +269,29 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - #357, where non utf-8 files would break Firefly. -- Tab delimiter is not properly loaded from import configuration (@roberthorlings) +- Tab delimiter is not properly loaded from import configuration ([roberthorlings](https://github.com/roberthorlings)) - System response to yearly bills ## [4.0.2] - 2016-10-14 ### Added -- Added ``intl`` dependency to composer file to ease installation (thanks @telyn) +- Added ``intl`` dependency to composer file to ease installation (thanks [telyn](https://github.com/telyn)) - Added support for Croatian. ### Changed - Updated all copyright notices to refer to the [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/) - Fixed #344 -- Fixed #346, thanks to @SanderKleykens +- Fixed #346, thanks to [SanderKleykens](https://github.com/SanderKleykens) - #351 - Did some internal remodelling. ### Fixed -- PostgreSQL compatibility thanks to @SanderKleykens -- @RobertHorlings fixed a bug in the ABN Amro import specific. +- PostgreSQL compatibility thanks to [SanderKleykens](https://github.com/SanderKleykens) +- [roberthorlings](https://github.com/roberthorlings) fixed a bug in the ABN Amro import specific. ## [4.0.1] - 2016-10-04 ### Added -- New ING import specific by @tomwerf +- New ING import specific by [tomwerf](https://github.com/tomwerf) - New Presidents Choice specific to fix #307 - Added some trimming (#335) @@ -59,10 +306,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed a bug where incoming transactions would not be properly filtered in several reports. -- #334 by @cyberkov +- #334 by [cyberkov](https://github.com/cyberkov) - #337 - #336 -- #338 found by @roberthorlings +- #338 found by [roberthorlings](https://github.com/roberthorlings) ### Security - Initial release. @@ -73,29 +320,29 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [4.0.0] - 2015-09-26 ### Added - Upgraded to Laravel 5.3, most other libraries upgraded as well. -- Added GBP as currency, thanks to @Mortalife +- Added GBP as currency, thanks to [Mortalife](https://github.com/Mortalife) ### Changed - Jump to version 4.0.0. - Firefly III is now subject to a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/) license. Previous versions of this software are still MIT licensed. ### Fixed -- Support for specific decimal places, thanks to @Mortalife +- Support for specific decimal places, thanks to [Mortalife](https://github.com/Mortalife) - Various CSS fixes -- Various bugs, thanks to @fuf, @sandermulders and @vissert +- Various bugs, thanks to [fuf](https://github.com/fuf), [sandermulders](https://github.com/sandermulders) and [vissert](https://github.com/vissert) - Various queries optimized for MySQL 5.7 ## [3.10.4] - 2015-09-14 ### Fixed -- Migration fix by @sandermulders -- Tricky import bug fix thanks to @vissert -- Currency preference will be correctly pulled from user settings, thanks to @fuf +- Migration fix by [sandermulders](https://github.com/sandermulders) +- Tricky import bug fix thanks to [vissert](https://github.com/vissert) +- Currency preference will be correctly pulled from user settings, thanks to [fuf](https://github.com/fuf) - Simplified code for upgrade instructions. ## [3.10.3] - 2016-08-29 ### Added -- More fields for mass-edit, thanks to @Vissert (#282) +- More fields for mass-edit, thanks to [vissert](https://github.com/vissert) (#282) - First start of German translation ### Changed @@ -106,7 +353,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - A bug in the translation routine broke the import. -- It was possible to destroy your Firefly installation by removing all currencies. Thanks @mondjef +- It was possible to destroy your Firefly installation by removing all currencies. Thanks [mondjef](https://github.com/mondjef) - Translation bugs. - Import bug. @@ -126,8 +373,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Bug in the mass edit routines. -- Firefly III over a proxy will now work (see [issue #290](https://github.com/JC5/firefly-iii/issues/290)), thanks @dfiel for reporting. -- Sneaky bug in the import routine, fixed by @Bonno +- Firefly III over a proxy will now work (see [issue #290](https://github.com/firefly-iii/firefly-iii/issues/290)), thanks [dfiel](https://github.com/dfiel) for reporting. +- Sneaky bug in the import routine, fixed by [Bonno](https://github.com/Bonno) ## [3.10.1] - 2016-08-25 ### Added @@ -147,7 +394,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Fixed a bug where a migration would check an empty table name. - Fixed various bugs in the import routine. - Fixed various bugs in the piggy banks pages. -- Fixed a bug in the ``firefly:verify`` routine +- Fixed a bug in the `firefly:verify` routine ## [3.10] - 2015-05-25 ### Added @@ -165,25 +412,25 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Issue #264 - Issue #265 -- Fixed amount calculation problems, #266, thanks @xzaz +- Fixed amount calculation problems, #266, thanks [xzaz](https://github.com/xzaz) - Issue #271 -- Issue #278, #273, thanks @StevenReitsma and @rubella +- Issue #278, #273, thanks [StevenReitsma](https://github.com/StevenReitsma) and [rubella](https://github.com/rubella) - Bug in attachment download routine would report the wrong size to the user's browser. - Various NULL errors fixed. - Various strict typing errors fixed. -- Fixed pagination problems, #276, thanks @xzaz +- Fixed pagination problems, #276, thanks [xzaz](https://github.com/xzaz) - Fixed a bug where an expense would be assigned to a piggy bank if you created a transfer first. -- Bulk update problems, #280, thanks @stickgrinder +- Bulk update problems, #280, thanks [stickgrinder](https://github.com/stickgrinder) - Fixed various problems with amount reporting of split transactions. -[3.9.1] +## [3.9.1] ### Fixed - Fixed a bug where removing money from a piggy bank would not work. See issue #265 and #269 -[3.9.0] +## [3.9.0] ### Added -- @zjean has added code that allows you to force "https://"-URL's. -- @tonicospinelli has added Portuguese (Brazil) translations. +- [zjean](https://github.com/zjean) has added code that allows you to force "https://"-URL's. +- [tonicospinelli](https://github.com/tonicospinelli) has added Portuguese (Brazil) translations. - Firefly III supports the *splitting* of transactions: - A withdrawal (expense) can be split into multiple sub-transactions (with multiple destinations) - Likewise for deposits (incomes). You can set multiple sources. @@ -214,7 +461,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Several CSV related bugs. - Several other bugs. -- Bugs fixed by @Bonno. +- Bugs fixed by [Bonno](https://github.com/Bonno). ## [3.8.3] - 2016-04-17 ### Added @@ -261,7 +508,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - Two factor authentication, thanks to the excellent work of [zjean](https://github.com/zjean). - A new chart showing your net worth in year and multi-year reports. -- You can now see if your current or future rules actually match any transactions, thanks to the excellent work of @roberthorlings. +- You can now see if your current or future rules actually match any transactions, thanks to the excellent work of [roberthorlings](https://github.com/roberthorlings). - New date fields for transactions. They are not used yet in reports or anything, but they can be filled in. - New routine to export your data. - Firefly III will mail the site owner when blocked users try to login, or when blocked domains are used in registrations. diff --git a/Dockerfile b/Dockerfile index 38d92f5bef..7d00a3724b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,12 +11,17 @@ RUN apt-get update -y && \ libtidy-dev \ libxml2-dev \ libsqlite3-dev \ - libbz2-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 +# 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 + # Enable apache mod rewrite.. RUN a2enmod rewrite @@ -40,3 +45,5 @@ WORKDIR /var/www/firefly-iii RUN composer install --no-scripts --no-dev USER root + +ENTRYPOINT ["/var/www/firefly-iii/docker/entrypoint.sh"] diff --git a/README.md b/README.md index a62386c40a..5013f18eab 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,20 @@ -# Firefly III [![Requires PHP7](https://img.shields.io/badge/php-7.0-red.svg)](https://secure.php.net/downloads.php#v7.0.4) [![Latest Stable Version](https://poser.pugx.org/grumpydictator/firefly-iii/v/stable)](https://packagist.org/packages/grumpydictator/firefly-iii) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/JC5/firefly-iii/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/JC5/firefly-iii/?branch=master) +# Firefly III: A personal finances manager -## A personal finances manager +[![Requires PHP7](https://img.shields.io/badge/php-7.0-red.svg)](https://secure.php.net/downloads.php#v7.0.4) [![Latest Stable Version](https://poser.pugx.org/grumpydictator/firefly-iii/v/stable)](https://packagist.org/packages/grumpydictator/firefly-iii) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/firefly-iii/firefly-iii/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/firefly-iii/firefly-iii/?branch=master) [![Build Status](https://travis-ci.org/firefly-iii/firefly-iii.svg?branch=master)](https://travis-ci.org/firefly-iii/firefly-iii) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=44UKUT455HUFA) -[![Screenshot](https://i.nder.be/hhfv03hp/400)](https://i.nder.be/hhfv03hp) [![Screenshot](https://i.nder.be/hhmwmqw9/400)](https://i.nder.be/hhmwmqw9) +[![The index of Firefly III](https://i.nder.be/hurdhgyg/400)](https://i.nder.be/h2b37243) [![The account overview of Firefly III](https://i.nder.be/hnkfkdpr/400)](https://i.nder.be/hv70pbwc) -[![Screenshot](https://i.nder.be/g63q05m0/400)](https://i.nder.be/g63q05m0) [![Screenshot](https://i.nder.be/c2g30ngg/400)](https://i.nder.be/c2g30ngg) +[![The useful financial reports of Firefly III](https://i.nder.be/h7sk6nb7/400)](https://i.nder.be/ccn0u2mp) [![Even more useful reports in Firefly III](https://i.nder.be/g237hr35/400)](https://i.nder.be/gm8hbh7z) "Firefly III" is a financial manager. It can help you keep track of expenses, income, budgets and everything in between. It even supports credit cards, shared household accounts and savings accounts! It's pretty fancy. You should use it to save and organise money. +## Try it out! + +Try out Firefly III on the [demo site](https://firefly-iii.nder.be/). + ## Installation -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://jc5.github.io/firefly-iii/installation-guide/). +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/installation-guide/). ## More about Firefly III @@ -22,9 +26,11 @@ Firefly works on the principle that if you know where you're money is going, you - 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! -- Firefly has lots of features without becoming fancy or bloated. +- Firefly has lots of features without being fancy or bloated. - If you feel you're missing something you can just ask me and I'll add it! -Firefly is pretty awesome. [You can read more about Firefly III, and its features, on the Github Pages](https://jc5.github.io/firefly-iii/). +Firefly is pretty awesome. [You can read more about Firefly III, and its features, on the Github Pages](https://firefly-iii.github.io/). -If you want to contact me, please open an issue or [email me](mailto:thegrumpydictator@gmail.com). \ No newline at end of file +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 0bf65903fa..00ff5094c6 100644 --- a/app/Console/Commands/CreateImport.php +++ b/app/Console/Commands/CreateImport.php @@ -50,13 +50,10 @@ class CreateImport extends Command } /** - * Execute the console command. - * - * @return mixed + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) // cannot be helped */ public function handle() { - // find the file /** @var UserRepositoryInterface $userRepository */ $userRepository = app(UserRepositoryInterface::class); $file = $this->argument('file'); @@ -69,7 +66,6 @@ class CreateImport extends Command return; } - // try to parse configuration data: $configurationData = json_decode(file_get_contents($configuration)); if (is_null($configurationData)) { $this->error(sprintf('Firefly III cannot read the contents of configuration file "%s" (working directory: "%s").', $configuration, $cwd)); @@ -83,22 +79,19 @@ class CreateImport extends Command $this->info(sprintf('Type of import: %s', $type)); /** @var ImportJobRepositoryInterface $jobRepository */ - $jobRepository = app(ImportJobRepositoryInterface::class, [$user]); - - $job = $jobRepository->create($type); + $jobRepository = app(ImportJobRepositoryInterface::class); + $jobRepository->setUser($user); + $job = $jobRepository->create($type); $this->line(sprintf('Created job "%s"...', $job->key)); - // put the file in the proper place: Artisan::call('firefly:encrypt', ['file' => $file, 'key' => $job->key]); $this->line('Stored import data...'); - // store the configuration in the job: $job->configuration = $configurationData; $job->status = 'settings_complete'; $job->save(); $this->line('Stored configuration...'); - // if user wants to run it, do! if ($this->option('start') === true) { $this->line('The import will start in a moment. This process is not visible...'); Log::debug('Go for import!'); @@ -111,10 +104,10 @@ class CreateImport extends Command /** * @return bool + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's five exactly. */ private function validArguments(): bool { - // find the file /** @var UserRepositoryInterface $userRepository */ $userRepository = app(UserRepositoryInterface::class); $file = $this->argument('file'); diff --git a/app/Console/Commands/EncryptFile.php b/app/Console/Commands/EncryptFile.php index 415649f9a4..1ff271a9e5 100644 --- a/app/Console/Commands/EncryptFile.php +++ b/app/Console/Commands/EncryptFile.php @@ -35,7 +35,7 @@ class EncryptFile extends Command * * @var string */ - protected $signature = 'firefly:encrypt {file} {key}'; + protected $signature = 'firefly:encrypt-file {file} {key}'; /** * Create a new command instance. diff --git a/app/Console/Commands/Import.php b/app/Console/Commands/Import.php index 0459e1c75b..c67f39000f 100644 --- a/app/Console/Commands/Import.php +++ b/app/Console/Commands/Import.php @@ -18,6 +18,7 @@ use FireflyIII\Import\Logging\CommandHandler; use FireflyIII\Models\ImportJob; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; +use Illuminate\Support\Collection; use Log; /** @@ -51,9 +52,7 @@ class Import extends Command } /** - * Execute the console command. * - * @return mixed */ public function handle() { @@ -66,37 +65,20 @@ class Import extends Command return; } - $this->line('Going to import job with key "' . $job->key . '" of type ' . $job->file_type); + $this->line(sprintf('Going to import job with key "%s" of type "%s"', $job->key, $job->file_type)); $monolog = Log::getMonolog(); $handler = new CommandHandler($this); $monolog->pushHandler($handler); + $importProcedure = new ImportProcedure; + $result = $importProcedure->runImport($job); - $result = ImportProcedure::runImport($job); - - - /** - * @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)); - } - + // display result to user: + $this->presentResults($result); $this->line('The import has completed.'); // get any errors from the importer: - $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); - } - } + $this->presentErrors($job); return; } @@ -122,4 +104,36 @@ 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/ScanAttachments.php b/app/Console/Commands/ScanAttachments.php index 04254afb6f..0dc03631f1 100644 --- a/app/Console/Commands/ScanAttachments.php +++ b/app/Console/Commands/ScanAttachments.php @@ -60,42 +60,26 @@ class ScanAttachments extends Command /** @var Attachment $attachment */ foreach ($attachments as $attachment) { $fileName = $attachment->fileName(); - - // try to grab file content: try { $content = $disk->get($fileName); } catch (FileNotFoundException $e) { $this->error(sprintf('Could not find data for attachment #%d', $attachment->id)); continue; } - // try to decrypt content. try { $decrypted = Crypt::decrypt($content); } catch (DecryptException $e) { $this->error(sprintf('Could not decrypt data of attachment #%d', $attachment->id)); continue; } - - // make temp file: $tmpfname = tempnam(sys_get_temp_dir(), 'FireflyIII'); - - // store content in temp file: file_put_contents($tmpfname, $decrypted); - - // get md5 and mime - $md5 = md5_file($tmpfname); - $mime = mime_content_type($tmpfname); - - // update attachment: + $md5 = md5_file($tmpfname); + $mime = mime_content_type($tmpfname); $attachment->md5 = $md5; $attachment->mime = $mime; $attachment->save(); - - $this->line(sprintf('Fixed attachment #%d', $attachment->id)); - - // find file: - } } } diff --git a/app/Console/Commands/UpgradeDatabase.php b/app/Console/Commands/UpgradeDatabase.php index 5b0ed46a5f..7b3e905b88 100644 --- a/app/Console/Commands/UpgradeDatabase.php +++ b/app/Console/Commands/UpgradeDatabase.php @@ -15,11 +15,14 @@ namespace FireflyIII\Console\Commands; use DB; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\LimitRepetition; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; use Illuminate\Database\QueryException; use Log; +use Schema; /** * Class UpgradeDatabase @@ -56,8 +59,29 @@ class UpgradeDatabase extends Command public function handle() { $this->setTransactionIdentifier(); + $this->migrateRepetitions(); + } - + private function migrateRepetitions() + { + if (!Schema::hasTable('budget_limits')) { + return; + } + // get all budget limits with end_date NULL + $set = BudgetLimit::whereNull('end_date')->get(); + $this->line(sprintf('Found %d budget limit(s) to update', $set->count())); + /** @var BudgetLimit $budgetLimit */ + foreach ($set as $budgetLimit) { + // get limit repetition (should be just one): + /** @var LimitRepetition $repetition */ + $repetition = $budgetLimit->limitrepetitions()->first(); + if (!is_null($repetition)) { + $budgetLimit->end_date = $repetition->enddate; + $budgetLimit->save(); + $this->line(sprintf('Updated budget limit #%d', $budgetLimit->id)); + $repetition->delete(); + } + } } /** @@ -65,12 +89,17 @@ class UpgradeDatabase extends Command */ private function setTransactionIdentifier() { - $subQuery = TransactionJournal - ::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->whereNull('transaction_journals.deleted_at') - ->whereNull('transactions.deleted_at') - ->groupBy(['transaction_journals.id']) - ->select(['transaction_journals.id', DB::raw('COUNT(transactions.id) AS t_count')]); + // if table does not exist, return false + if (!Schema::hasTable('transaction_journals')) { + return; + } + + + $subQuery = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->whereNull('transaction_journals.deleted_at') + ->whereNull('transactions.deleted_at') + ->groupBy(['transaction_journals.id']) + ->select(['transaction_journals.id', DB::raw('COUNT(transactions.id) AS t_count')]); $result = DB::table(DB::raw('(' . $subQuery->toSql() . ') AS derived')) ->mergeBindings($subQuery->getQuery()) @@ -79,43 +108,52 @@ class UpgradeDatabase extends Command $journalIds = array_unique($result->pluck('id')->toArray()); foreach ($journalIds as $journalId) { - // grab all positive transactiosn from this journal that are not deleted. - // for each one, grab the negative opposing one which has 0 as an identifier and give it the same identifier. - $identifier = 0; - $processed = []; - $transactions = Transaction::where('transaction_journal_id', $journalId)->where('amount', '>', 0)->get(); - /** @var Transaction $transaction */ - foreach ($transactions as $transaction) { - // find opposing: - $amount = bcmul(strval($transaction->amount), '-1'); + $this->updateJournal(intval($journalId)); + } + } - try { - /** @var Transaction $opposing */ - $opposing = Transaction - ::where('transaction_journal_id', $journalId) - ->where('amount', $amount)->where('identifier', '=', 0) - ->whereNotIn('id', $processed) - ->first(); - } catch (QueryException $e) { - Log::error($e->getMessage()); - $this->error('Firefly III could not find the "identifier" field in the "transactions" table.'); - $this->error('This field is required for Firefly III version ' . config('firefly.version') . ' to run.'); - $this->error('Please run "php artisan migrate" to add this field to the table.'); - $this->info('Then, run "php artisan firefly:upgrade-database" to try again.'); - break 2; - } - if (!is_null($opposing)) { - // give both a new identifier: - $transaction->identifier = $identifier; - $transaction->save(); - $opposing->identifier = $identifier; - $opposing->save(); - $processed[] = $transaction->id; - $processed[] = $opposing->id; - $this->line(sprintf('Database upgrade for journal #%d, transactions #%d and #%d', $journalId, $transaction->id, $opposing->id)); - } - $identifier++; + /** + * grab all positive transactiosn from this journal that are not deleted. for each one, grab the negative opposing one + * which has 0 as an identifier and give it the same identifier. + * + * @param int $journalId + */ + private function updateJournal(int $journalId) + { + $identifier = 0; + $processed = []; + $transactions = Transaction::where('transaction_journal_id', $journalId)->where('amount', '>', 0)->get(); + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + // find opposing: + $amount = bcmul(strval($transaction->amount), '-1'); + + try { + /** @var Transaction $opposing */ + $opposing = Transaction::where('transaction_journal_id', $journalId) + ->where('amount', $amount)->where('identifier', '=', 0) + ->whereNotIn('id', $processed) + ->first(); + } catch (QueryException $e) { + Log::error($e->getMessage()); + $this->error('Firefly III could not find the "identifier" field in the "transactions" table.'); + $this->error(sprintf('This field is required for Firefly III version %s to run.', config('firefly.version'))); + $this->error('Please run "php artisan migrate" to add this field to the table.'); + $this->info('Then, run "php artisan firefly:upgrade-database" to try again.'); + + return; } + if (!is_null($opposing)) { + // give both a new identifier: + $transaction->identifier = $identifier; + $transaction->save(); + $opposing->identifier = $identifier; + $opposing->save(); + $processed[] = $transaction->id; + $processed[] = $opposing->id; + $this->line(sprintf('Database upgrade for journal #%d, transactions #%d and #%d', $journalId, $transaction->id, $opposing->id)); + } + $identifier++; } } } diff --git a/app/Console/Commands/UpgradeFireflyInstructions.php b/app/Console/Commands/UpgradeFireflyInstructions.php index 2a4a801ade..a88c8ab79c 100644 --- a/app/Console/Commands/UpgradeFireflyInstructions.php +++ b/app/Console/Commands/UpgradeFireflyInstructions.php @@ -33,7 +33,7 @@ class UpgradeFireflyInstructions extends Command * * @var string */ - protected $signature = 'firefly:upgrade-instructions'; + protected $signature = 'firefly:instructions {task}'; /** * Create a new command instance. @@ -49,11 +49,60 @@ class UpgradeFireflyInstructions extends Command */ public function handle() { - // + + if ($this->argument('task') == 'update') { + $this->updateInstructions(); + } + if ($this->argument('task') == 'install') { + $this->installInstructions(); + } + } + + /** + * Show a nice box + * + * @param string $text + */ + private function boxed(string $text) + { + $parts = explode("\n", wordwrap($text)); + foreach ($parts as $string) { + $this->line('| ' . sprintf('%-77s', $string) . '|'); + } + } + + /** + * Show a nice info box + * + * @param string $text + */ + private function boxedInfo(string $text) + { + $parts = explode("\n", wordwrap($text)); + foreach ($parts as $string) { + $this->info('| ' . sprintf('%-77s', $string) . '|'); + } + } + + /** + * Show a line + */ + private function showLine() + { + $line = '+'; + for ($i = 0; $i < 78; $i++) { + $line .= '-'; + } + $line .= '+'; + $this->line($line); + + } + + private function installInstructions() { /** @var string $version */ $version = config('firefly.version'); - $config = config('upgrade.text'); - $text = null; + $config = config('upgrade.text.install'); + $text = ''; foreach (array_keys($config) as $compare) { // if string starts with: $len = strlen($compare); @@ -62,22 +111,53 @@ class UpgradeFireflyInstructions extends Command } } - - $this->line('+------------------------------------------------------------------------------+'); - $this->line(''); - + $this->showLine(); + $this->boxed(''); if (is_null($text)) { - $this->line('Thank you for installing Firefly III, v' . $version); - $this->info('There are no extra upgrade instructions.'); - $this->line('Firefly III should be ready for use.'); - } else { - $this->line('Thank you for installing Firefly III, v' . $version); - $this->line('If you are upgrading from a previous version,'); - $this->line('please follow these upgrade instructions carefully:'); - $this->info(wordwrap($text)); + + $this->boxed(sprintf('Thank you for installin Firefly III, v%s!', $version)); + $this->boxedInfo('There are no extra installation instructions.'); + $this->boxed('Firefly III should be ready for use.'); + $this->boxed(''); + $this->showLine(); + return; } - $this->line(''); - $this->line('+------------------------------------------------------------------------------+'); + $this->boxed(sprintf('Thank you for installing Firefly III, v%s!', $version)); + $this->boxedInfo($text); + $this->boxed(''); + $this->showLine(); + } + + private function updateInstructions() + { + /** @var string $version */ + $version = config('firefly.version'); + $config = config('upgrade.text.upgrade'); + $text = ''; + foreach (array_keys($config) as $compare) { + // if string starts with: + $len = strlen($compare); + if (substr($version, 0, $len) === $compare) { + $text = $config[$compare]; + } + + } + $this->showLine(); + $this->boxed(''); + if (is_null($text)) { + + $this->boxed(sprintf('Thank you for updating to Firefly III, v%s', $version)); + $this->boxedInfo('There are no extra upgrade instructions.'); + $this->boxed('Firefly III should be ready for use.'); + $this->boxed(''); + $this->showLine(); + return; + } + + $this->boxed(sprintf('Thank you for updating to Firefly III, v%s!', $version)); + $this->boxedInfo($text); + $this->boxed(''); + $this->showLine(); } } diff --git a/app/Console/Commands/UseEncryption.php b/app/Console/Commands/UseEncryption.php new file mode 100644 index 0000000000..d3c3e178c0 --- /dev/null +++ b/app/Console/Commands/UseEncryption.php @@ -0,0 +1,66 @@ +handleObjects('Account', 'name', 'encrypted'); + $this->handleObjects('Bill', 'name', 'name_encrypted'); + $this->handleObjects('Bill', 'match', 'match_encrypted'); + $this->handleObjects('Budget', 'name', 'encrypted'); + $this->handleObjects('Category', 'name', 'encrypted'); + $this->handleObjects('PiggyBank', 'name', 'encrypted'); + $this->handleObjects('TransactionJournal', 'description', 'encrypted'); + } + + /** + * @param string $class + * @param string $field + * @param string $indicator + */ + public function handleObjects(string $class, string $field, string $indicator) + { + $fqn = sprintf('FireflyIII\Models\%s', $class); + $encrypt = config('firefly.encryption') ? 0 : 1; + $set = $fqn::where($indicator, $encrypt)->get(); + + foreach ($set as $entry) { + $newName = $entry->$field; + $entry->$field = $newName; + $entry->save(); + } + + $this->line(sprintf('Updated %d %s.', $set->count(), strtolower(Str::plural($class)))); + } +} diff --git a/app/Console/Commands/VerifyDatabase.php b/app/Console/Commands/VerifyDatabase.php index 8571d599af..8c73968caa 100644 --- a/app/Console/Commands/VerifyDatabase.php +++ b/app/Console/Commands/VerifyDatabase.php @@ -17,15 +17,15 @@ use Crypt; 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\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Console\Command; +use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Database\Eloquent\Builder; +use Schema; use stdClass; /** @@ -61,16 +61,21 @@ class VerifyDatabase extends Command */ public function handle() { + // if table does not exist, return false + if (!Schema::hasTable('users')) { + return; + } + + $this->reportObject('budget'); + $this->reportObject('category'); + $this->reportObject('tag'); + // accounts with no transactions. $this->reportAccounts(); // budgets with no limits $this->reportBudgetLimits(); // budgets with no transactions - $this->reportBudgets(); - // categories with no transactions - $this->reportCategories(); - // tags with no transactions - $this->reportTags(); + // sum of transactions is not zero. $this->reportSum(); // any deleted transaction journals that have transactions that are NOT deleted: @@ -95,14 +100,13 @@ class VerifyDatabase extends Command */ private function reportAccounts() { - $set = Account - ::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id') - ->leftJoin('users', 'accounts.user_id', '=', 'users.id') - ->groupBy(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email']) - ->whereNull('transactions.account_id') - ->get( - ['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'] - ); + $set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id') + ->leftJoin('users', 'accounts.user_id', '=', 'users.id') + ->groupBy(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email']) + ->whereNull('transactions.account_id') + ->get( + ['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'] + ); /** @var stdClass $entry */ foreach ($set as $entry) { @@ -118,59 +122,18 @@ class VerifyDatabase extends Command */ private function reportBudgetLimits() { - $set = Budget - ::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id') - ->leftJoin('users', 'budgets.user_id', '=', 'users.id') - ->groupBy(['budgets.id', 'budgets.name', 'budgets.user_id', 'users.email']) - ->whereNull('budget_limits.id') - ->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'users.email']); + $set = Budget::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id') + ->leftJoin('users', 'budgets.user_id', '=', 'users.id') + ->groupBy(['budgets.id', 'budgets.name', 'budgets.encrypted', 'budgets.user_id', 'users.email']) + ->whereNull('budget_limits.id') + ->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'budgets.encrypted', 'users.email']); - /** @var stdClass $entry */ + /** @var Budget $entry */ foreach ($set as $entry) { - $line = 'Notice: User #' . $entry->user_id . ' (' . $entry->email . ') has budget #' . $entry->id . ' ("' . Crypt::decrypt($entry->name) - . '") which has no budget limits.'; - $this->line($line); - } - } - - /** - * Reports on budgets without any transactions. - */ - private function reportBudgets() - { - $set = Budget - ::leftJoin('budget_transaction_journal', 'budgets.id', '=', 'budget_transaction_journal.budget_id') - ->leftJoin('users', 'budgets.user_id', '=', 'users.id') - ->distinct() - ->whereNull('budget_transaction_journal.budget_id') - ->whereNull('budgets.deleted_at') - ->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'users.email']); - - /** @var stdClass $entry */ - foreach ($set as $entry) { - $line = 'Notice: User #' . $entry->user_id . ' (' . $entry->email . ') has budget #' . $entry->id . ' ("' . Crypt::decrypt($entry->name) - . '") which has no transactions.'; - $this->line($line); - } - } - - /** - * Reports on categories without any transactions. - */ - private function reportCategories() - { - $set = Category - ::leftJoin('category_transaction_journal', 'categories.id', '=', 'category_transaction_journal.category_id') - ->leftJoin('users', 'categories.user_id', '=', 'users.id') - ->distinct() - ->whereNull('category_transaction_journal.category_id') - ->whereNull('categories.deleted_at') - ->get(['categories.id', 'categories.name', 'categories.user_id', 'users.email']); - - /** @var stdClass $entry */ - foreach ($set as $entry) { - $line = 'Notice: User #' . $entry->user_id . ' (' . $entry->email . ') has category #' . $entry->id . ' ("' . Crypt::decrypt($entry->name) - . '") which has no transactions.'; + $line = sprintf( + 'Notice: User #%d (%s) has budget #%d ("%s") which has no budget limits.', + $entry->user_id, $entry->email, $entry->id, $entry->name + ); $this->line($line); } } @@ -180,22 +143,21 @@ class VerifyDatabase extends Command */ private function reportDeletedAccounts() { - $set = Account - ::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id') - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->whereNotNull('accounts.deleted_at') - ->whereNotNull('transactions.id') - ->where( - function (Builder $q) { - $q->whereNull('transactions.deleted_at'); - $q->orWhereNull('transaction_journals.deleted_at'); - } - ) - ->get( - ['accounts.id as account_id', 'accounts.deleted_at as account_deleted_at', 'transactions.id as transaction_id', - 'transactions.deleted_at as transaction_deleted_at', 'transaction_journals.id as journal_id', - 'transaction_journals.deleted_at as journal_deleted_at'] - ); + $set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id') + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->whereNotNull('accounts.deleted_at') + ->whereNotNull('transactions.id') + ->where( + function (Builder $q) { + $q->whereNull('transactions.deleted_at'); + $q->orWhereNull('transaction_journals.deleted_at'); + } + ) + ->get( + ['accounts.id as account_id', 'accounts.deleted_at as account_deleted_at', 'transactions.id as transaction_id', + 'transactions.deleted_at as transaction_deleted_at', 'transaction_journals.id as journal_id', + 'transaction_journals.deleted_at as journal_deleted_at'] + ); /** @var stdClass $entry */ foreach ($set as $entry) { $date = is_null($entry->transaction_deleted_at) ? $entry->journal_deleted_at : $entry->transaction_deleted_at; @@ -211,24 +173,24 @@ class VerifyDatabase extends Command $configuration = [ // a withdrawal can not have revenue account: TransactionType::WITHDRAWAL => [AccountType::REVENUE], - // deposit cannot have an expense account: TransactionType::DEPOSIT => [AccountType::EXPENSE], - // transfer cannot have either: TransactionType::TRANSFER => [AccountType::EXPENSE, AccountType::REVENUE], ]; foreach ($configuration as $transactionType => $accountTypes) { - $set = TransactionJournal - ::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') - ->leftJoin('account_types', 'account_types.id', 'accounts.account_type_id') - ->leftJoin('users', 'users.id', '=', 'transaction_journals.user_id') - ->where('transaction_types.type', $transactionType) - ->whereIn('account_types.type', $accountTypes) - ->whereNull('transaction_journals.deleted_at') - ->get(['transaction_journals.id', 'transaction_journals.user_id', 'users.email', 'account_types.type as a_type', 'transaction_types.type']); + $set = TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') + ->leftJoin('account_types', 'account_types.id', 'accounts.account_type_id') + ->leftJoin('users', 'users.id', '=', 'transaction_journals.user_id') + ->where('transaction_types.type', $transactionType) + ->whereIn('account_types.type', $accountTypes) + ->whereNull('transaction_journals.deleted_at') + ->get( + ['transaction_journals.id', 'transaction_journals.user_id', 'users.email', 'account_types.type as a_type', + 'transaction_types.type'] + ); foreach ($set as $entry) { $this->error( sprintf( @@ -250,19 +212,18 @@ class VerifyDatabase extends Command */ private function reportJournals() { - $set = TransactionJournal - ::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->whereNotNull('transaction_journals.deleted_at')// USE THIS - ->whereNull('transactions.deleted_at') - ->whereNotNull('transactions.id') - ->get( - [ - 'transaction_journals.id as journal_id', - 'transaction_journals.description', - 'transaction_journals.deleted_at as journal_deleted', - 'transactions.id as transaction_id', - 'transactions.deleted_at as transaction_deleted_at'] - ); + $set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->whereNotNull('transaction_journals.deleted_at')// USE THIS + ->whereNull('transactions.deleted_at') + ->whereNotNull('transactions.id') + ->get( + [ + 'transaction_journals.id as journal_id', + 'transaction_journals.description', + 'transaction_journals.deleted_at as journal_deleted', + 'transactions.id as transaction_id', + 'transactions.deleted_at as transaction_deleted_at'] + ); /** @var stdClass $entry */ foreach ($set as $entry) { $this->error( @@ -277,11 +238,10 @@ class VerifyDatabase extends Command */ private function reportNoTransactions() { - $set = TransactionJournal - ::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->groupBy('transaction_journals.id') - ->whereNull('transactions.transaction_journal_id') - ->get(['transaction_journals.id']); + $set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->groupBy('transaction_journals.id') + ->whereNull('transactions.transaction_journal_id') + ->get(['transaction_journals.id']); foreach ($set as $entry) { $this->error( @@ -291,6 +251,39 @@ class VerifyDatabase extends Command } + /** + * @param string $name + */ + private function reportObject(string $name) + { + $plural = str_plural($name); + $class = sprintf('FireflyIII\Models\%s', ucfirst($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() + ->whereNull($name . '_transaction_journal.' . $name . '_id') + ->whereNull($plural . '.deleted_at') + ->get([$plural . '.id', $plural . '.' . $field . ' as name', $plural . '.user_id', 'users.email']); + + /** @var stdClass $entry */ + foreach ($set as $entry) { + + $objName = $entry->name; + try { + $objName = Crypt::decrypt($objName); + } catch (DecryptException $e) { + // it probably was not encrypted. + } + + $line = sprintf( + 'Notice: User #%d (%s) has %s #%d ("%s") which has no transactions.', + $entry->user_id, $entry->email, $name, $entry->id, $objName + ); + $this->line($line); + } + } + /** * Reports for each user when the sum of their transactions is not zero. */ @@ -308,40 +301,18 @@ class VerifyDatabase extends Command } } - /** - * Reports on tags without any transactions. - */ - private function reportTags() - { - $set = Tag - ::leftJoin('tag_transaction_journal', 'tags.id', '=', 'tag_transaction_journal.tag_id') - ->leftJoin('users', 'tags.user_id', '=', 'users.id') - ->distinct() - ->whereNull('tag_transaction_journal.tag_id') - ->whereNull('tags.deleted_at') - ->get(['tags.id', 'tags.tag', 'tags.user_id', 'users.email']); - - /** @var stdClass $entry */ - foreach ($set as $entry) { - $line = 'Notice: User #' . $entry->user_id . ' (' . $entry->email . ') has tag #' . $entry->id . ' ("' . $entry->tag - . '") which has no transactions.'; - $this->line($line); - } - } - /** * Reports on deleted transactions that are connected to a not deleted journal. */ private function reportTransactions() { - $set = Transaction - ::leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->whereNotNull('transactions.deleted_at') - ->whereNull('transaction_journals.deleted_at') - ->get( - ['transactions.id as transaction_id', 'transactions.deleted_at as transaction_deleted', 'transaction_journals.id as journal_id', - 'transaction_journals.deleted_at'] - ); + $set = Transaction::leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->whereNotNull('transactions.deleted_at') + ->whereNull('transaction_journals.deleted_at') + ->get( + ['transactions.id as transaction_id', 'transactions.deleted_at as transaction_deleted', 'transaction_journals.id as journal_id', + 'transaction_journals.deleted_at'] + ); /** @var stdClass $entry */ foreach ($set as $entry) { $this->error( @@ -356,12 +327,11 @@ class VerifyDatabase extends Command */ private function reportTransfersBudgets() { - $set = TransactionJournal - ::distinct() - ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->leftJoin('budget_transaction_journal', 'transaction_journals.id', '=', 'budget_transaction_journal.transaction_journal_id') - ->where('transaction_types.type', TransactionType::TRANSFER) - ->whereNotNull('budget_transaction_journal.budget_id')->get(['transaction_journals.id']); + $set = TransactionJournal::distinct() + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->leftJoin('budget_transaction_journal', 'transaction_journals.id', '=', 'budget_transaction_journal.transaction_journal_id') + ->where('transaction_types.type', TransactionType::TRANSFER) + ->whereNotNull('budget_transaction_journal.budget_id')->get(['transaction_journals.id']); /** @var TransactionJournal $entry */ foreach ($set as $entry) { diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php old mode 100755 new mode 100644 index d62f5371ca..f4a1e32559 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -19,8 +19,8 @@ use FireflyIII\Console\Commands\Import; use FireflyIII\Console\Commands\ScanAttachments; use FireflyIII\Console\Commands\UpgradeDatabase; use FireflyIII\Console\Commands\UpgradeFireflyInstructions; +use FireflyIII\Console\Commands\UseEncryption; use FireflyIII\Console\Commands\VerifyDatabase; -use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; /** @@ -39,9 +39,9 @@ class Kernel extends ConsoleKernel */ protected $bootstrappers = [ - 'Illuminate\Foundation\Bootstrap\DetectEnvironment', + 'Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables', 'Illuminate\Foundation\Bootstrap\LoadConfiguration', - 'FireflyIII\Bootstrap\ConfigureLogging', + //'FireflyIII\Bootstrap\ConfigureLogging', 'Illuminate\Foundation\Bootstrap\HandleExceptions', 'Illuminate\Foundation\Bootstrap\RegisterFacades', 'Illuminate\Foundation\Bootstrap\SetRequestForConsole', @@ -63,7 +63,7 @@ class Kernel extends ConsoleKernel EncryptFile::class, ScanAttachments::class, UpgradeDatabase::class, - + UseEncryption::class, ]; /** @@ -75,15 +75,4 @@ class Kernel extends ConsoleKernel { require base_path('routes/console.php'); } - - /** - * Define the application's command schedule. - * - * @param \Illuminate\Console\Scheduling\Schedule $schedule - * - * @return void - */ - protected function schedule(Schedule $schedule) - { - } } diff --git a/app/Events/ConfirmedUser.php b/app/Events/RequestedNewPassword.php similarity index 70% rename from app/Events/ConfirmedUser.php rename to app/Events/RequestedNewPassword.php index 20fe797488..52919a646b 100644 --- a/app/Events/ConfirmedUser.php +++ b/app/Events/RequestedNewPassword.php @@ -1,6 +1,6 @@ user = $user; + $this->token = $token; $this->ipAddress = $ipAddress; } + } diff --git a/app/Events/ResentConfirmation.php b/app/Events/ResentConfirmation.php deleted file mode 100644 index 1ce53d51b3..0000000000 --- a/app/Events/ResentConfirmation.php +++ /dev/null @@ -1,42 +0,0 @@ -user = $user; - $this->ipAddress = $ipAddress; - } -} diff --git a/app/Events/StoredBudgetLimit.php b/app/Events/StoredBudgetLimit.php deleted file mode 100644 index 65b7be0008..0000000000 --- a/app/Events/StoredBudgetLimit.php +++ /dev/null @@ -1,50 +0,0 @@ -budgetLimit = $budgetLimit; - $this->end = $end; - - } - -} diff --git a/app/Events/UpdatedBudgetLimit.php b/app/Events/UpdatedBudgetLimit.php deleted file mode 100644 index 3642531e75..0000000000 --- a/app/Events/UpdatedBudgetLimit.php +++ /dev/null @@ -1,50 +0,0 @@ -budgetLimit = $budgetLimit; - $this->end = $end; - - } - -} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php old mode 100755 new mode 100644 index 4f9d8b6675..00b359d148 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -21,6 +21,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Session\TokenMismatchException; use Illuminate\Validation\ValidationException as ValException; +use Request; use Symfony\Component\HttpKernel\Exception\HttpException; /** @@ -72,12 +73,14 @@ class Handler extends ExceptionHandler * This is a great spot to send exceptions to Sentry, Bugsnag, etc. * * @param Exception $exception + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. * * @return void */ public function report(Exception $exception) { - if ($exception instanceof FireflyException || $exception instanceof ErrorException) { + $doMailError = env('SEND_ERROR_MESSAGE', true); + if (($exception instanceof FireflyException || $exception instanceof ErrorException) && $doMailError) { $userData = [ 'id' => 0, 'email' => 'unknown@example.com', @@ -97,8 +100,8 @@ class Handler extends ExceptionHandler ]; // create job that will mail. - $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; - $job = new MailError($userData, env('SITE_OWNER', ''), $ip, $data); + $ipAddress = Request::ip() ?? '0.0.0.0'; + $job = new MailError($userData, env('SITE_OWNER', ''), $ipAddress, $data); dispatch($job); } @@ -108,9 +111,9 @@ class Handler extends ExceptionHandler /** * Convert an authentication exception into an unauthenticated response. * - * @param \Illuminate\Http\Request $request + * @param $request * - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse */ protected function unauthenticated($request) { diff --git a/app/Export/Collector/AttachmentCollector.php b/app/Export/Collector/AttachmentCollector.php index e068f32585..810dc1db85 100644 --- a/app/Export/Collector/AttachmentCollector.php +++ b/app/Export/Collector/AttachmentCollector.php @@ -43,10 +43,8 @@ class AttachmentCollector extends BasicCollector implements CollectorInterface /** * AttachmentCollector constructor. - * - * @param ExportJob $job */ - public function __construct(ExportJob $job) + public function __construct() { /** @var AttachmentRepositoryInterface repository */ $this->repository = app(AttachmentRepositoryInterface::class); @@ -54,7 +52,7 @@ class AttachmentCollector extends BasicCollector implements CollectorInterface $this->uploadDisk = Storage::disk('upload'); $this->exportDisk = Storage::disk('export'); - parent::__construct($job); + parent::__construct(); } /** diff --git a/app/Export/Collector/BasicCollector.php b/app/Export/Collector/BasicCollector.php index 4fd571c84d..c80e8ec280 100644 --- a/app/Export/Collector/BasicCollector.php +++ b/app/Export/Collector/BasicCollector.php @@ -31,13 +31,10 @@ class BasicCollector /** * BasicCollector constructor. - * - * @param ExportJob $job */ - public function __construct(ExportJob $job) + public function __construct() { $this->entries = new Collection; - $this->job = $job; } /** @@ -56,5 +53,13 @@ class BasicCollector $this->entries = $entries; } + /** + * @param ExportJob $job + */ + public function setJob(ExportJob $job) + { + $this->job = $job; + } + } diff --git a/app/Export/Collector/CollectorInterface.php b/app/Export/Collector/CollectorInterface.php index 6fc24233fe..54a3fa88a1 100644 --- a/app/Export/Collector/CollectorInterface.php +++ b/app/Export/Collector/CollectorInterface.php @@ -13,6 +13,7 @@ declare(strict_types = 1); namespace FireflyIII\Export\Collector; +use FireflyIII\Models\ExportJob; use Illuminate\Support\Collection; /** @@ -32,9 +33,18 @@ interface CollectorInterface */ public function run(): bool; + /** + * @param ExportJob $job + * + * @return mixed + */ + public function setJob(ExportJob $job); + /** * @param Collection $entries * + * @return void + * */ public function setEntries(Collection $entries); diff --git a/app/Export/Collector/JournalCollector.php b/app/Export/Collector/JournalExportCollector.php similarity index 66% rename from app/Export/Collector/JournalCollector.php rename to app/Export/Collector/JournalExportCollector.php index 7ea5e7ae5f..e9bfbca879 100644 --- a/app/Export/Collector/JournalCollector.php +++ b/app/Export/Collector/JournalExportCollector.php @@ -1,6 +1,6 @@ accounts->pluck('id')->toArray(); - $this->workSet = Transaction - ::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->leftJoin( - 'transactions AS opposing', function (JoinClause $join) { - $join->on('opposing.transaction_journal_id', '=', 'transactions.transaction_journal_id') - ->where('opposing.amount', '=', DB::raw('transactions.amount * -1')) - ->where('transactions.identifier', '=', 'opposing.identifier'); - } - ) - ->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') - ->whereIn('transactions.account_id', $accountIds) - ->where('transaction_journals.user_id', $this->job->user_id) - ->where('transaction_journals.date', '>=', $this->start->format('Y-m-d')) - ->where('transaction_journals.date', '<=', $this->end->format('Y-m-d')) - ->where('transaction_journals.completed', 1) - ->whereNull('transaction_journals.deleted_at') - ->whereNull('transactions.deleted_at') - ->whereNull('opposing.deleted_at') - ->orderBy('transaction_journals.date', 'DESC') - ->orderBy('transactions.identifier', 'ASC') - ->get( - [ - 'transactions.id', - 'transactions.amount', - 'transactions.description', - 'transactions.account_id', - 'accounts.name as account_name', - 'accounts.encrypted as account_name_encrypted', - 'transactions.identifier', + $this->workSet = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->leftJoin( + 'transactions AS opposing', function (JoinClause $join) { + $join->on('opposing.transaction_journal_id', '=', 'transactions.transaction_journal_id') + ->where('opposing.amount', '=', DB::raw('transactions.amount * -1')) + ->where('transactions.identifier', '=', DB::raw('opposing.identifier')); + } + ) + ->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') + ->whereIn('transactions.account_id', $accountIds) + ->where('transaction_journals.user_id', $this->job->user_id) + ->where('transaction_journals.date', '>=', $this->start->format('Y-m-d')) + ->where('transaction_journals.date', '<=', $this->end->format('Y-m-d')) + ->where('transaction_journals.completed', 1) + ->whereNull('transaction_journals.deleted_at') + ->whereNull('transactions.deleted_at') + ->whereNull('opposing.deleted_at') + ->orderBy('transaction_journals.date', 'DESC') + ->orderBy('transactions.identifier', 'ASC') + ->get( + [ + 'transactions.id', + 'transactions.amount', + 'transactions.description', + 'transactions.account_id', + 'accounts.name as account_name', + 'accounts.encrypted as account_name_encrypted', + 'transactions.identifier', - 'opposing.id as opposing_id', - 'opposing.amount AS opposing_amount', - 'opposing.description as opposing_description', - 'opposing.account_id as opposing_account_id', - 'opposing_accounts.name as opposing_account_name', - 'opposing_accounts.encrypted as opposing_account_encrypted', - 'opposing.identifier as opposing_identifier', + 'opposing.id as opposing_id', + 'opposing.amount AS opposing_amount', + 'opposing.description as opposing_description', + 'opposing.account_id as opposing_account_id', + 'opposing_accounts.name as opposing_account_name', + 'opposing_accounts.encrypted as opposing_account_encrypted', + 'opposing.identifier as opposing_identifier', - 'transaction_journals.id as transaction_journal_id', - 'transaction_journals.date', - 'transaction_journals.description as journal_description', - 'transaction_journals.encrypted as journal_encrypted', - 'transaction_journals.transaction_type_id', - 'transaction_types.type as transaction_type', - 'transaction_journals.transaction_currency_id', - 'transaction_currencies.code AS transaction_currency_code', + 'transaction_journals.id as transaction_journal_id', + 'transaction_journals.date', + 'transaction_journals.description as journal_description', + 'transaction_journals.encrypted as journal_encrypted', + 'transaction_journals.transaction_type_id', + 'transaction_types.type as transaction_type', + 'transaction_journals.transaction_currency_id', + 'transaction_currencies.code AS transaction_currency_code', - ] - ); + ] + ); } } diff --git a/app/Export/Collector/UploadCollector.php b/app/Export/Collector/UploadCollector.php index c4d030a10c..35c2e75188 100644 --- a/app/Export/Collector/UploadCollector.php +++ b/app/Export/Collector/UploadCollector.php @@ -14,7 +14,6 @@ declare(strict_types = 1); namespace FireflyIII\Export\Collector; use Crypt; -use FireflyIII\Models\ExportJob; use Illuminate\Contracts\Encryption\DecryptException; use Log; use Storage; @@ -35,22 +34,12 @@ class UploadCollector extends BasicCollector implements CollectorInterface /** * AttachmentCollector constructor. - * - * @param ExportJob $job */ - public function __construct(ExportJob $job) + public function __construct() { - parent::__construct($job); - - Log::debug('Going to collect attachments', ['key' => $job->key]); - - // make storage: + parent::__construct(); $this->uploadDisk = Storage::disk('upload'); $this->exportDisk = Storage::disk('export'); - - // file names associated with the old import routine. - $this->vintageFormat = sprintf('csv-upload-%d-', auth()->user()->id); - } /** @@ -60,6 +49,11 @@ class UploadCollector extends BasicCollector implements CollectorInterface */ public function run(): bool { + Log::debug('Going to collect attachments', ['key' => $this->job->key]); + + // file names associated with the old import routine. + $this->vintageFormat = sprintf('csv-upload-%d-', $this->job->user->id); + // collect old upload files (names beginning with "csv-upload". $this->collectVintageUploads(); @@ -94,7 +88,7 @@ class UploadCollector extends BasicCollector implements CollectorInterface * * @return bool */ - private function collectVintageUploads():bool + private function collectVintageUploads(): bool { // grab upload directory. $files = $this->uploadDisk->files(); diff --git a/app/Export/Entry/Entry.php b/app/Export/Entry/Entry.php index 259dd675ab..0afc40ada4 100644 --- a/app/Export/Entry/Entry.php +++ b/app/Export/Entry/Entry.php @@ -29,6 +29,7 @@ use Crypt; * * * Class Entry + * @SuppressWarnings(PHPMD.LongVariable) * * @package FireflyIII\Export\Entry */ @@ -50,7 +51,6 @@ final class Entry public $destination_account_id; public $destination_account_name; - public $budget_id; public $budget_name; public $category_id; @@ -71,33 +71,21 @@ final class Entry */ public static function fromObject($object): Entry { - $entry = new self; - - // journal information: - $entry->journal_id = $object->transaction_journal_id; - $entry->description = $object->journal_encrypted === 1 ? Crypt::decrypt($object->journal_description) : $object->journal_description; - $entry->amount = round($object->amount, 2); // always positive - $entry->date = $object->date; - $entry->transaction_type = $object->transaction_type; - $entry->currency_code = $object->transaction_currency_code; - - // source information: - $entry->source_account_id = $object->account_id; - $entry->source_account_name = $object->account_name_encrypted === 1 ? Crypt::decrypt($object->account_name) : $object->account_name; - - - // destination information + $entry = new self; + $entry->journal_id = $object->transaction_journal_id; + $entry->description = self::decrypt(intval($object->journal_encrypted), $object->journal_description); + $entry->amount = $object->amount; + $entry->date = $object->date; + $entry->transaction_type = $object->transaction_type; + $entry->currency_code = $object->transaction_currency_code; + $entry->source_account_id = $object->account_id; + $entry->source_account_name = self::decrypt(intval($object->account_name_encrypted), $object->account_name); $entry->destination_account_id = $object->opposing_account_id; - $entry->destination_account_name = $object->opposing_account_encrypted === 1 ? Crypt::decrypt($object->opposing_account_name) - : $object->opposing_account_name; - - - // category and budget - $entry->category_id = $object->category_id ?? ''; - $entry->category_name = $object->category_name ?? ''; - $entry->budget_id = $object->budget_id ?? ''; - $entry->budget_name = $object->budget_name ?? ''; - + $entry->destination_account_name = self::decrypt(intval($object->opposing_account_encrypted), $object->opposing_account_name); + $entry->category_id = $object->category_id ?? ''; + $entry->category_name = $object->category_name ?? ''; + $entry->budget_id = $object->budget_id ?? ''; + $entry->budget_name = $object->budget_name ?? ''; // update description when transaction description is different: if (!is_null($object->description) && $object->description != $entry->description) { @@ -107,4 +95,19 @@ final class Entry return $entry; } + /** + * @param int $isEncrypted + * @param $value + * + * @return string + */ + protected static function decrypt(int $isEncrypted, $value) + { + if ($isEncrypted === 1) { + return Crypt::decrypt($value); + } + + return $value; + } + } diff --git a/app/Export/Exporter/BasicExporter.php b/app/Export/Exporter/BasicExporter.php index b16b19776a..256fa3b544 100644 --- a/app/Export/Exporter/BasicExporter.php +++ b/app/Export/Exporter/BasicExporter.php @@ -26,17 +26,15 @@ class BasicExporter { /** @var ExportJob */ protected $job; - private $entries; + /** @var Collection */ + private $entries; /** * BasicExporter constructor. - * - * @param ExportJob $job */ - public function __construct(ExportJob $job) + public function __construct() { $this->entries = new Collection; - $this->job = $job; } /** @@ -55,5 +53,13 @@ class BasicExporter $this->entries = $entries; } + /** + * @param ExportJob $job + */ + public function setJob(ExportJob $job) + { + $this->job = $job; + } + } diff --git a/app/Export/Exporter/CsvExporter.php b/app/Export/Exporter/CsvExporter.php index 0091b55ff1..200bf8116d 100644 --- a/app/Export/Exporter/CsvExporter.php +++ b/app/Export/Exporter/CsvExporter.php @@ -14,8 +14,6 @@ declare(strict_types = 1); namespace FireflyIII\Export\Exporter; use FireflyIII\Export\Entry\Entry; -use FireflyIII\Export\Entry\EntryAccount; -use FireflyIII\Models\ExportJob; use League\Csv\Writer; use SplFileObject; @@ -31,13 +29,10 @@ class CsvExporter extends BasicExporter implements ExporterInterface /** * CsvExporter constructor. - * - * @param ExportJob $job */ - public function __construct(ExportJob $job) + public function __construct() { - parent::__construct($job); - + parent::__construct(); } /** diff --git a/app/Export/Exporter/ExporterInterface.php b/app/Export/Exporter/ExporterInterface.php index 9b8a4e4ed9..9e267b4812 100644 --- a/app/Export/Exporter/ExporterInterface.php +++ b/app/Export/Exporter/ExporterInterface.php @@ -13,6 +13,7 @@ declare(strict_types = 1); namespace FireflyIII\Export\Exporter; +use FireflyIII\Models\ExportJob; use Illuminate\Support\Collection; /** @@ -40,7 +41,14 @@ interface ExporterInterface /** * @param Collection $entries * + * @return void + * */ public function setEntries(Collection $entries); + /** + * @param ExportJob $job + */ + public function setJob(ExportJob $job); + } diff --git a/app/Export/Processor.php b/app/Export/Processor.php index 88669c6d72..cbbdd2736b 100644 --- a/app/Export/Processor.php +++ b/app/Export/Processor.php @@ -15,11 +15,10 @@ namespace FireflyIII\Export; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Export\Collector\AttachmentCollector; -use FireflyIII\Export\Collector\JournalCollector; +use FireflyIII\Export\Collector\JournalExportCollector; use FireflyIII\Export\Collector\UploadCollector; use FireflyIII\Export\Entry\Entry; use FireflyIII\Models\ExportJob; -use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Support\Collection; use Log; use Storage; @@ -30,7 +29,7 @@ use ZipArchive; * * @package FireflyIII\Export */ -class Processor +class Processor implements ProcessorInterface { /** @var Collection */ @@ -45,8 +44,6 @@ class Processor public $job; /** @var array */ public $settings; - /** @var \FireflyIII\Export\ConfigurationFile */ - private $configurationMaker; /** @var Collection */ private $exportEntries; /** @var Collection */ @@ -56,21 +53,12 @@ class Processor /** * Processor constructor. - * - * @param array $settings */ - public function __construct(array $settings) + public function __construct() { - // save settings - $this->settings = $settings; - $this->accounts = $settings['accounts']; - $this->exportFormat = $settings['exportFormat']; - $this->includeAttachments = $settings['includeAttachments']; - $this->includeOldUploads = $settings['includeOldUploads']; - $this->job = $settings['job']; - $this->journals = new Collection; - $this->exportEntries = new Collection; - $this->files = new Collection; + $this->journals = new Collection; + $this->exportEntries = new Collection; + $this->files = new Collection; } @@ -80,7 +68,8 @@ class Processor public function collectAttachments(): bool { /** @var AttachmentCollector $attachmentCollector */ - $attachmentCollector = app(AttachmentCollector::class, [$this->job]); + $attachmentCollector = app(AttachmentCollector::class); + $attachmentCollector->setJob($this->job); $attachmentCollector->setDates($this->settings['startDate'], $this->settings['endDate']); $attachmentCollector->run(); $this->files = $this->files->merge($attachmentCollector->getEntries()); @@ -93,8 +82,9 @@ class Processor */ public function collectJournals(): bool { - /** @var JournalCollector $collector */ - $collector = app(JournalCollector::class, [$this->job]); + /** @var JournalExportCollector $collector */ + $collector = app(JournalExportCollector::class); + $collector->setJob($this->job); $collector->setDates($this->settings['startDate'], $this->settings['endDate']); $collector->setAccounts($this->settings['accounts']); $collector->run(); @@ -110,7 +100,8 @@ class Processor public function collectOldUploads(): bool { /** @var UploadCollector $uploadCollector */ - $uploadCollector = app(UploadCollector::class, [$this->job]); + $uploadCollector = app(UploadCollector::class); + $uploadCollector->setJob($this->job); $uploadCollector->run(); $this->files = $this->files->merge($uploadCollector->getEntries()); @@ -157,7 +148,7 @@ class Processor $zip->close(); // delete the files: - $this->deleteFiles($disk); + $this->deleteFiles(); return true; } @@ -168,7 +159,8 @@ class Processor public function exportJournals(): bool { $exporterClass = config('firefly.export_formats.' . $this->exportFormat); - $exporter = app($exporterClass, [$this->job]); + $exporter = app($exporterClass); + $exporter->setJob($this->job); $exporter->setEntries($this->exportEntries); $exporter->run(); $this->files->push($exporter->getFileName()); @@ -185,10 +177,25 @@ class Processor } /** - * @param FilesystemAdapter $disk + * @param array $settings */ - private function deleteFiles(FilesystemAdapter $disk) + public function setSettings(array $settings) { + // save settings + $this->settings = $settings; + $this->accounts = $settings['accounts']; + $this->exportFormat = $settings['exportFormat']; + $this->includeAttachments = $settings['includeAttachments']; + $this->includeOldUploads = $settings['includeOldUploads']; + $this->job = $settings['job']; + } + + /** + * + */ + private function deleteFiles() + { + $disk = Storage::disk('export'); foreach ($this->getFiles() as $file) { $disk->delete($file); } diff --git a/app/Export/ProcessorInterface.php b/app/Export/ProcessorInterface.php new file mode 100644 index 0000000000..540dbcaf37 --- /dev/null +++ b/app/Export/ProcessorInterface.php @@ -0,0 +1,70 @@ + 1, - 'labels' => [], 'datasets' => [[ - 'label' => trans('firefly.spent'), - 'data' => []]]]; - foreach ($accounts as $account) { - if ($account->difference > 0) { - $data['labels'][] = $account->name; - $data['datasets'][0]['data'][] = $account->difference; - } - } - - return $data; - } - - /** - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end - * - * @return array - */ - public function frontpage(Collection $accounts, Carbon $start, Carbon $end): array - { - // language: - $format = (string)trans('config.month_and_day'); - $data = ['count' => 0, 'labels' => [], 'datasets' => [],]; - $current = clone $start; - while ($current <= $end) { - $data['labels'][] = $current->formatLocalized($format); - $current->addDay(); - } - - foreach ($accounts as $account) { - $data['datasets'][] = [ - 'label' => $account->name, - 'fillColor' => 'rgba(220,220,220,0.2)', - 'strokeColor' => 'rgba(220,220,220,1)', - 'pointColor' => 'rgba(220,220,220,1)', - 'pointStrokeColor' => '#fff', - 'pointHighlightFill' => '#fff', - 'pointHighlightStroke' => 'rgba(220,220,220,1)', - 'data' => $account->balances, - ]; - } - $data['count'] = count($data['datasets']); - - return $data; - } - - /** - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end - * - * @return array - */ - public function revenueAccounts(Collection $accounts, Carbon $start, Carbon $end): array - { - $data = [ - 'count' => 1, - 'labels' => [], 'datasets' => [[ - 'label' => trans('firefly.earned'), - 'data' => []]]]; - foreach ($accounts as $account) { - if ($account->difference > 0) { - $data['labels'][] = $account->name; - $data['datasets'][0]['data'][] = $account->difference; - } - } - - return $data; - } - - /** - * @param Account $account - * @param array $labels - * @param array $dataSet - * - * @return array - */ - public function single(Account $account, array $labels, array $dataSet): array - { - $data = [ - 'count' => 1, - 'labels' => $labels, - 'datasets' => [ - [ - 'label' => $account->name, - 'data' => $dataSet, - ], - ], - ]; - - return $data; - } -} diff --git a/app/Generator/Chart/Basic/ChartJsGenerator.php b/app/Generator/Chart/Basic/ChartJsGenerator.php new file mode 100644 index 0000000000..b7588d2da2 --- /dev/null +++ b/app/Generator/Chart/Basic/ChartJsGenerator.php @@ -0,0 +1,147 @@ + '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' + * ] + * ] + * 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 + * + * @return array + */ + public function multiSet(array $data): array + { + reset($data); + $first = current($data); + $labels = is_array($first['entries']) ? array_keys($first['entries']) : []; + + $chartData = [ + 'count' => count($data), + 'labels' => $labels, // take ALL labels from the first set. + 'datasets' => [], + ]; + unset($first, $labels); + + foreach ($data as $set) { + $currentSet = [ + 'label' => $set['label'], + 'type' => $set['type'] ?? 'line', + 'data' => array_values($set['entries']), + ]; + if (isset($set['yAxisID'])) { + $currentSet['yAxisID'] = $set['yAxisID']; + } + if (isset($set['fill'])) { + $currentSet['fill'] = $set['fill']; + } + + $chartData['datasets'][] = $currentSet; + } + + return $chartData; + } + + /** + * Expects data as: + * + * key => value + * + * @param array $data + * + * @return array + */ + public function pieChart(array $data): array + { + $chartData = [ + 'datasets' => [ + 0 => [], + ], + 'labels' => [], + ]; + $index = 0; + foreach ($data as $key => $value) { + + // make larger than 0 + $chartData['datasets'][0]['data'][] = floatval(Steam::positive($value)); + $chartData['datasets'][0]['backgroundColor'][] = ChartColour::getColour($index); + $chartData['labels'][] = $key; + $index++; + } + + return $chartData; + } + + /** + * Will generate a (ChartJS) compatible array from the given input. Expects this format: + * + * 'label-of-entry' => value + * 'label-of-entry' => value + * + * @param string $setLabel + * @param array $data + * + * @return array + */ + public function singleSet(string $setLabel, array $data): array + { + $chartData = [ + 'count' => 1, + 'labels' => array_keys($data), // take ALL labels from the first set. + 'datasets' => [ + [ + 'label' => $setLabel, + 'data' => array_values($data), + ], + ], + ]; + + return $chartData; + } +} diff --git a/app/Generator/Chart/Basic/GeneratorInterface.php b/app/Generator/Chart/Basic/GeneratorInterface.php new file mode 100644 index 0000000000..8b4b90af8a --- /dev/null +++ b/app/Generator/Chart/Basic/GeneratorInterface.php @@ -0,0 +1,73 @@ + 'label of set', + * 'entries' => + * [ + * 'label-of-entry' => 'value' + * ] + * ] + * 1: [ + * 'label' => 'label of another set', + * 'entries' => + * [ + * 'label-of-entry' => 'value' + * ] + * ] + * + * + * @param array $data + * + * @return array + */ + public function multiSet(array $data): array; + + /** + * Expects data as: + * + * key => value + * + * @param array $data + * + * @return array + */ + public function pieChart(array $data): array; + + /** + * Will generate a (ChartJS) compatible array from the given input. Expects this format: + * + * 'label-of-entry' => value + * 'label-of-entry' => value + * + * @param string $setLabel + * @param array $data + * + * @return array + */ + public function singleSet(string $setLabel, array $data): array; + +} diff --git a/app/Generator/Chart/Bill/BillChartGeneratorInterface.php b/app/Generator/Chart/Bill/BillChartGeneratorInterface.php deleted file mode 100644 index 27b1c11cef..0000000000 --- a/app/Generator/Chart/Bill/BillChartGeneratorInterface.php +++ /dev/null @@ -1,44 +0,0 @@ - [ - [ - 'data' => [round($unpaid, 2), round(bcmul($paid, '-1'), 2)], - 'backgroundColor' => ['rgba(53, 124, 165,0.7)', 'rgba(0, 141, 76, 0.7)',], - ], - - ], - 'labels' => [strval(trans('firefly.unpaid')), strval(trans('firefly.paid'))], - - ]; - - return $data; - } - - /** - * @param Bill $bill - * @param Collection $entries - * - * @return array - */ - public function single(Bill $bill, Collection $entries): array - { - $format = (string)trans('config.month'); - $data = ['count' => 3, 'labels' => [], 'datasets' => [],]; - $minAmount = []; - $maxAmount = []; - $actualAmount = []; - /** @var TransactionJournal $entry */ - foreach ($entries as $entry) { - $data['labels'][] = $entry->date->formatLocalized($format); - $minAmount[] = round($bill->amount_min, 2); - $maxAmount[] = round($bill->amount_max, 2); - // journalAmount has been collected in BillRepository::getJournals - $actualAmount[] = round(TransactionJournal::amountPositive($entry), 2); - } - - $data['datasets'][] = [ - 'type' => 'bar', - 'label' => trans('firefly.minAmount'), - 'data' => $minAmount, - ]; - $data['datasets'][] = [ - 'type' => 'line', - 'label' => trans('firefly.billEntry'), - 'data' => $actualAmount, - ]; - $data['datasets'][] = [ - 'type' => 'bar', - 'label' => trans('firefly.maxAmount'), - 'data' => $maxAmount, - ]; - - $data['count'] = count($data['datasets']); - - return $data; - } -} diff --git a/app/Generator/Chart/Budget/BudgetChartGeneratorInterface.php b/app/Generator/Chart/Budget/BudgetChartGeneratorInterface.php deleted file mode 100644 index fc466aa418..0000000000 --- a/app/Generator/Chart/Budget/BudgetChartGeneratorInterface.php +++ /dev/null @@ -1,64 +0,0 @@ - [], - 'datasets' => [ - [ - 'label' => 'Amount', - 'data' => [], - ], - ], - ]; - - /** @var array $entry */ - foreach ($entries as $entry) { - $data['labels'][] = $entry[0]->formatLocalized($format); - $data['datasets'][0]['data'][] = $entry[1]; - - } - - $data['count'] = count($data['datasets']); - - return $data; - } - - /** - * @param Collection $entries - * - * @return array - */ - public function frontpage(Collection $entries): array - { - $data = [ - 'count' => 0, - 'labels' => [], - 'datasets' => [], - ]; - $left = []; - $spent = []; - $overspent = []; - $filtered = $entries->filter( - function ($entry) { - return ($entry[1] != 0 || $entry[2] != 0 || $entry[3] != 0); - } - ); - foreach ($filtered as $entry) { - $data['labels'][] = $entry[0]; - $left[] = round($entry[1], 2); - $spent[] = round(bcmul($entry[2], '-1'), 2); // spent is coming in negative, must be positive - $overspent[] = round(bcmul($entry[3], '-1'), 2); // same - } - - $data['datasets'][] = [ - 'label' => trans('firefly.overspent'), - 'data' => $overspent, - ]; - $data['datasets'][] = [ - 'label' => trans('firefly.left'), - 'data' => $left, - ]; - $data['datasets'][] = [ - 'label' => trans('firefly.spent'), - 'data' => $spent, - ]; - - $data['count'] = 3; - - return $data; - } - - /** - * @param Collection $entries - * - * @return array - */ - public function multiYear(Collection $entries): array - { - // dataset: - $data = [ - 'count' => 0, - 'labels' => [], - 'datasets' => [], - ]; - // get labels from one of the budgets (assuming there's at least one): - $first = $entries->first(); - $keys = array_keys($first['budgeted']); - foreach ($keys as $year) { - $data['labels'][] = strval($year); - } - - // then, loop all entries and create datasets: - foreach ($entries as $entry) { - $name = $entry['name']; - $spent = $entry['spent']; - $budgeted = $entry['budgeted']; - $data['datasets'][] = ['label' => 'Spent on ' . $name, 'data' => array_values($spent)]; - $data['datasets'][] = ['label' => 'Budgeted for ' . $name, 'data' => array_values($budgeted)]; - } - $data['count'] = count($data['datasets']); - - return $data; - - } - - /** - * @param Collection $entries - * @param string $viewRange - * - * @return array - */ - public function period(Collection $entries, string $viewRange) : array - { - $data = [ - 'labels' => [], - 'datasets' => [ - 0 => [ - 'label' => trans('firefly.budgeted'), - 'data' => [], - ], - 1 => [ - 'label' => trans('firefly.spent'), - 'data' => [], - ], - ], - 'count' => 2, - ]; - foreach ($entries as $entry) { - $label = Navigation::periodShow($entry['date'], $viewRange); - $data['labels'][] = $label; - // data set 0 is budgeted - // data set 1 is spent: - $data['datasets'][0]['data'][] = $entry['budgeted']; - $data['datasets'][1]['data'][] = round(($entry['spent'] * -1), 2); - - } - - return $data; - - } - - /** - * @param Collection $budgets - * @param Collection $entries - * - * @return array - */ - public function year(Collection $budgets, Collection $entries): array - { - // language: - $format = (string)trans('config.month'); - - $data = [ - 'labels' => [], - 'datasets' => [], - ]; - - foreach ($budgets as $budget) { - $data['labels'][] = $budget->name; - } - // also add "no budget" - $data['labels'][] = strval(trans('firefly.no_budget')); - - /** @var array $entry */ - foreach ($entries as $entry) { - $array = [ - 'label' => $entry[0]->formatLocalized($format), - 'data' => [], - ]; - array_shift($entry); - $array['data'] = $entry; - $data['datasets'][] = $array; - - } - $data['count'] = count($data['datasets']); - - return $data; - } -} diff --git a/app/Generator/Chart/Category/CategoryChartGeneratorInterface.php b/app/Generator/Chart/Category/CategoryChartGeneratorInterface.php deleted file mode 100644 index 56d57f6966..0000000000 --- a/app/Generator/Chart/Category/CategoryChartGeneratorInterface.php +++ /dev/null @@ -1,69 +0,0 @@ - 2, - 'labels' => [], - 'datasets' => [ - [ - 'label' => trans('firefly.spent'), - 'data' => [], - ], - [ - 'label' => trans('firefly.earned'), - 'data' => [], - ], - ], - ]; - - foreach ($entries as $entry) { - $data['labels'][] = $entry[1]; - $spent = $entry[2]; - $earned = $entry[3]; - - $data['datasets'][0]['data'][] = bccomp($spent, '0') === 0 ? null : round(bcmul($spent, '-1'), 4); - $data['datasets'][1]['data'][] = bccomp($earned, '0') === 0 ? null : round($earned, 4); - } - - return $data; - } - - /** - * @param Collection $categories - * @param Collection $entries - * - * @return array - */ - public function earnedInPeriod(Collection $categories, Collection $entries): array - { - - // language: - $format = (string)trans('config.month'); - - $data = [ - 'count' => 0, - 'labels' => [], - 'datasets' => [], - ]; - - foreach ($categories as $category) { - $data['labels'][] = $category->name; - } - - foreach ($entries as $entry) { - $date = $entry[0]->formatLocalized($format); - array_shift($entry); - $data['count']++; - $data['datasets'][] = ['label' => $date, 'data' => $entry]; - } - - return $data; - - } - - /** - * @param Collection $entries - * - * @return array - */ - public function frontpage(Collection $entries): array - { - $data = [ - 'count' => 1, - 'labels' => [], - 'datasets' => [ - [ - 'label' => trans('firefly.spent'), - 'data' => [], - ], - ], - ]; - foreach ($entries as $entry) { - if ($entry->spent != 0) { - $data['labels'][] = $entry->name; - $data['datasets'][0]['data'][] = round(bcmul($entry->spent, '-1'), 2); - } - } - - return $data; - } - - /** - * @param Collection $entries - * - * @return array - */ - public function multiYear(Collection $entries): array - { - // get labels from one of the categories (assuming there's at least one): - $first = $entries->first(); - $data = ['count' => 0, 'labels' => array_keys($first['spent']), 'datasets' => [],]; - - // then, loop all entries and create datasets: - foreach ($entries as $entry) { - $name = $entry['name']; - $spent = $entry['spent']; - $earned = $entry['earned']; - if (array_sum(array_values($spent)) != 0) { - $data['datasets'][] = ['label' => 'Spent in category ' . $name, 'data' => array_values($spent)]; - } - if (array_sum(array_values($earned)) != 0) { - $data['datasets'][] = ['label' => 'Earned in category ' . $name, 'data' => array_values($earned)]; - } - } - $data['count'] = count($data['datasets']); - - return $data; - } - - /** - * - * @param Collection $entries - * - * @return array - */ - public function period(Collection $entries): array - { - return $this->all($entries); - - } - - /** - * @param Collection $categories - * @param Collection $entries - * - * @return array - */ - public function spentInPeriod(Collection $categories, Collection $entries): array - { - - // language: - $format = (string)trans('config.month'); - - $data = [ - 'count' => 0, - 'labels' => [], - 'datasets' => [], - ]; - - foreach ($categories as $category) { - $data['labels'][] = $category->name; - } - - foreach ($entries as $entry) { - $date = $entry[0]->formatLocalized($format); - array_shift($entry); - $data['count']++; - $data['datasets'][] = ['label' => $date, 'data' => $entry]; - } - - return $data; - - } -} diff --git a/app/Generator/Chart/PiggyBank/ChartJsPiggyBankChartGenerator.php b/app/Generator/Chart/PiggyBank/ChartJsPiggyBankChartGenerator.php deleted file mode 100644 index 2908fa0661..0000000000 --- a/app/Generator/Chart/PiggyBank/ChartJsPiggyBankChartGenerator.php +++ /dev/null @@ -1,58 +0,0 @@ - 1, - 'labels' => [], - 'datasets' => [ - [ - 'label' => 'Diff', - 'data' => [], - ], - ], - ]; - $sum = '0'; - foreach ($set as $key => $value) { - $date = new Carbon($key); - $sum = bcadd($sum, $value); - $data['labels'][] = $date->formatLocalized($format); - $data['datasets'][0]['data'][] = round($sum, 2); - } - - return $data; - } -} diff --git a/app/Generator/Chart/Report/ChartJsReportChartGenerator.php b/app/Generator/Chart/Report/ChartJsReportChartGenerator.php deleted file mode 100644 index 1cdb1bdedd..0000000000 --- a/app/Generator/Chart/Report/ChartJsReportChartGenerator.php +++ /dev/null @@ -1,180 +0,0 @@ - 2, - 'labels' => [], - 'datasets' => [ - [ - 'label' => trans('firefly.income'), - 'data' => [], - ], - [ - 'label' => trans('firefly.expenses'), - 'data' => [], - ], - ], - ]; - - foreach ($entries as $entry) { - $data['labels'][] = $entry[0]->formatLocalized('%Y'); - $data['datasets'][0]['data'][] = round($entry[1], 2); - $data['datasets'][1]['data'][] = round($entry[2], 2); - } - - return $data; - } - - /** - * @param string $income - * @param string $expense - * @param int $count - * - * @return array - */ - public function multiYearInOutSummarized(string $income, string $expense, int $count): array - { - $data = [ - 'count' => 2, - 'labels' => [trans('firefly.sum_of_years'), trans('firefly.average_of_years')], - 'datasets' => [ - [ - 'label' => trans('firefly.income'), - 'data' => [], - ], - [ - 'label' => trans('firefly.expenses'), - 'data' => [], - ], - ], - ]; - $data['datasets'][0]['data'][] = round($income, 2); - $data['datasets'][1]['data'][] = round($expense, 2); - $data['datasets'][0]['data'][] = round(($income / $count), 2); - $data['datasets'][1]['data'][] = round(($expense / $count), 2); - - return $data; - } - - /** - * @param Collection $entries - * - * @return array - */ - public function netWorth(Collection $entries) : array - { - $format = (string)trans('config.month_and_day'); - $data = [ - 'count' => 1, - 'labels' => [], - 'datasets' => [ - [ - 'label' => trans('firefly.net_worth'), - 'data' => [], - ], - ], - ]; - foreach ($entries as $entry) { - $data['labels'][] = trim($entry['date']->formatLocalized($format)); - $data['datasets'][0]['data'][] = round($entry['net-worth'], 2); - } - - return $data; - } - - /** - * @param Collection $entries - * - * @return array - */ - public function yearInOut(Collection $entries): array - { - // language: - $format = (string)trans('config.month'); - - $data = [ - 'count' => 2, - 'labels' => [], - 'datasets' => [ - [ - 'label' => trans('firefly.income'), - 'data' => [], - ], - [ - 'label' => trans('firefly.expenses'), - 'data' => [], - ], - ], - ]; - - foreach ($entries as $entry) { - $data['labels'][] = $entry[0]->formatLocalized($format); - $data['datasets'][0]['data'][] = round($entry[1], 2); - $data['datasets'][1]['data'][] = round($entry[2], 2); - } - - return $data; - } - - /** - * @param string $income - * @param string $expense - * @param int $count - * - * @return array - */ - public function yearInOutSummarized(string $income, string $expense, int $count): array - { - - $data = [ - 'count' => 2, - 'labels' => [trans('firefly.sum_of_year'), trans('firefly.average_of_year')], - 'datasets' => [ - [ - 'label' => trans('firefly.income'), - 'data' => [], - ], - [ - 'label' => trans('firefly.expenses'), - 'data' => [], - ], - ], - ]; - $data['datasets'][0]['data'][] = round($income, 2); - $data['datasets'][1]['data'][] = round($expense, 2); - $data['datasets'][0]['data'][] = round(($income / $count), 2); - $data['datasets'][1]['data'][] = round(($expense / $count), 2); - - return $data; - } -} diff --git a/app/Generator/Chart/Report/ReportChartGeneratorInterface.php b/app/Generator/Chart/Report/ReportChartGeneratorInterface.php deleted file mode 100644 index 021fad844a..0000000000 --- a/app/Generator/Chart/Report/ReportChartGeneratorInterface.php +++ /dev/null @@ -1,65 +0,0 @@ -start; + $dayBefore->subDay(); + /** @var Account $account */ + foreach ($this->accounts as $account) { + // balance the day before: + $id = $account->id; + $auditData[$id] = $this->getAuditReport($account, $dayBefore); + } + + $defaultShow = ['icon', 'description', 'balance_before', 'amount', 'balance_after', 'date', 'to']; + $reportType = 'audit'; + $accountIds = join(',', $this->accounts->pluck('id')->toArray()); + $hideable = ['buttons', 'icon', 'description', 'balance_before', 'amount', 'balance_after', 'date', + 'interest_date', 'book_date', 'process_date', + // three new optional fields. + 'due_date', 'payment_date', 'invoice_date', + 'from', 'to', 'budget', 'category', 'bill', + // more new optional fields + 'internal_reference', 'notes', + 'create_date', 'update_date', + ]; + + + return view('reports.audit.report', compact('reportType', 'accountIds', 'auditData', 'hideable', 'defaultShow')) + ->with('start', $this->start)->with('end', $this->end)->with('accounts', $this->accounts) + ->render(); + + } + + /** + * @param Collection $accounts + * + * @return ReportGeneratorInterface + */ + public function setAccounts(Collection $accounts): ReportGeneratorInterface + { + $this->accounts = $accounts; + + return $this; + } + + /** + * @param Collection $budgets + * + * @return ReportGeneratorInterface + */ + public function setBudgets(Collection $budgets): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Collection $categories + * + * @return ReportGeneratorInterface + */ + public function setCategories(Collection $categories): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setEndDate(Carbon $date): ReportGeneratorInterface + { + $this->end = $date; + + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setStartDate(Carbon $date): ReportGeneratorInterface + { + $this->start = $date; + + return $this; + } + + /** + * @param Collection $tags + * + * @return ReportGeneratorInterface + */ + public function setTags(Collection $tags): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Account $account + * @param Carbon $date + * + * @return array + */ + private function getAuditReport(Account $account, Carbon $date): array + { + + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts(new Collection([$account]))->setRange($this->start, $this->end); + $journals = $collector->getJournals(); + $journals = $journals->reverse(); + $dayBeforeBalance = Steam::balance($account, $date); + $startBalance = $dayBeforeBalance; + + + /** @var Transaction $journal */ + foreach ($journals as $transaction) { + $transaction->before = $startBalance; + $transactionAmount = $transaction->transaction_amount; + $newBalance = bcadd($startBalance, $transactionAmount); + $transaction->after = $newBalance; + $startBalance = $newBalance; + } + + /* + * Reverse set again. + */ + $return = [ + 'journals' => $journals->reverse(), + 'exists' => $journals->count() > 0, + 'end' => $this->end->formatLocalized(strval(trans('config.month_and_day'))), + 'endBalance' => Steam::balance($account, $this->end), + 'dayBefore' => $date->formatLocalized(strval(trans('config.month_and_day'))), + 'dayBeforeBalance' => $dayBeforeBalance, + ]; + + return $return; + } +} diff --git a/app/Generator/Report/Audit/MultiYearReportGenerator.php b/app/Generator/Report/Audit/MultiYearReportGenerator.php new file mode 100644 index 0000000000..f8baa6e66f --- /dev/null +++ b/app/Generator/Report/Audit/MultiYearReportGenerator.php @@ -0,0 +1,27 @@ +income = new Collection; + $this->expenses = new Collection; + } + + /** + * @return string + */ + public function generate(): string + { + $accountIds = join(',', $this->accounts->pluck('id')->toArray()); + $budgetIds = join(',', $this->budgets->pluck('id')->toArray()); + $expenses = $this->getExpenses(); + $accountSummary = $this->summarizeByAccount($expenses); + $budgetSummary = $this->summarizeByBudget($expenses); + $averageExpenses = $this->getAverages($expenses, SORT_ASC); + $topExpenses = $this->getTopExpenses(); + + // render! + return view('reports.budget.month', compact('accountIds', 'budgetIds', 'accountSummary', 'budgetSummary', 'averageExpenses', 'topExpenses')) + ->with('start', $this->start)->with('end', $this->end) + ->with('budgets', $this->budgets) + ->with('accounts', $this->accounts) + ->render(); + } + + /** + * @param Collection $accounts + * + * @return ReportGeneratorInterface + */ + public function setAccounts(Collection $accounts): ReportGeneratorInterface + { + $this->accounts = $accounts; + + return $this; + } + + /** + * @param Collection $budgets + * + * @return ReportGeneratorInterface + */ + public function setBudgets(Collection $budgets): ReportGeneratorInterface + { + $this->budgets = $budgets; + + return $this; + } + + /** + * @param Collection $categories + * + * @return ReportGeneratorInterface + */ + public function setCategories(Collection $categories): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setEndDate(Carbon $date): ReportGeneratorInterface + { + $this->end = $date; + + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setStartDate(Carbon $date): ReportGeneratorInterface + { + $this->start = $date; + + return $this; + } + + /** + * @param Collection $tags + * + * @return ReportGeneratorInterface + */ + public function setTags(Collection $tags): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Collection $collection + * @param int $sortFlag + * + * @return array + */ + private function getAverages(Collection $collection, int $sortFlag): array + { + $result = []; + /** @var Transaction $transaction */ + foreach ($collection as $transaction) { + // opposing name and ID: + $opposingId = $transaction->opposing_account_id; + + // is not set? + if (!isset($result[$opposingId])) { + $name = $transaction->opposing_account_name; + $result[$opposingId] = [ + 'name' => $name, + 'count' => 1, + 'id' => $opposingId, + 'average' => $transaction->transaction_amount, + 'sum' => $transaction->transaction_amount, + ]; + continue; + } + $result[$opposingId]['count']++; + $result[$opposingId]['sum'] = bcadd($result[$opposingId]['sum'], $transaction->transaction_amount); + $result[$opposingId]['average'] = bcdiv($result[$opposingId]['sum'], strval($result[$opposingId]['count'])); + } + + // sort result by average: + $average = []; + foreach ($result as $key => $row) { + $average[$key] = floatval($row['average']); + } + + array_multisort($average, $sortFlag, $result); + + return $result; + } + + /** + * @return Collection + */ + private function getExpenses(): Collection + { + if ($this->expenses->count() > 0) { + Log::debug('Return previous set of expenses.'); + + return $this->expenses; + } + + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($this->accounts)->setRange($this->start, $this->end) + ->setTypes([TransactionType::WITHDRAWAL]) + ->setBudgets($this->budgets)->withOpposingAccount()->disableFilter(); + + $accountIds = $this->accounts->pluck('id')->toArray(); + $transactions = $collector->getJournals(); + $transactions = self::filterExpenses($transactions, $accountIds); + $this->expenses = $transactions; + + return $transactions; + } + + /** + * @return Collection + */ + private function getTopExpenses(): Collection + { + $transactions = $this->getExpenses()->sortBy('transaction_amount'); + + return $transactions; + } + + /** + * @param Collection $collection + * + * @return array + */ + private function summarizeByAccount(Collection $collection): array + { + $result = []; + /** @var Transaction $transaction */ + foreach ($collection as $transaction) { + $accountId = $transaction->account_id; + $result[$accountId] = $result[$accountId] ?? '0'; + $result[$accountId] = bcadd($transaction->transaction_amount, $result[$accountId]); + } + + return $result; + } + + /** + * @param Collection $collection + * + * @return array + */ + private function summarizeByBudget(Collection $collection): array + { + $result = []; + /** @var Transaction $transaction */ + foreach ($collection as $transaction) { + $jrnlBudId = intval($transaction->transaction_journal_budget_id); + $transBudId = intval($transaction->transaction_budget_id); + $budgetId = max($jrnlBudId, $transBudId); + $result[$budgetId] = $result[$budgetId] ?? '0'; + $result[$budgetId] = bcadd($transaction->transaction_amount, $result[$budgetId]); + } + + return $result; + } +} diff --git a/app/Generator/Report/Budget/MultiYearReportGenerator.php b/app/Generator/Report/Budget/MultiYearReportGenerator.php new file mode 100644 index 0000000000..b3820f88dd --- /dev/null +++ b/app/Generator/Report/Budget/MultiYearReportGenerator.php @@ -0,0 +1,27 @@ +income = new Collection; + $this->expenses = new Collection; + } + + /** + * @return string + */ + public function generate(): string + { + $accountIds = join(',', $this->accounts->pluck('id')->toArray()); + $categoryIds = join(',', $this->categories->pluck('id')->toArray()); + $reportType = 'category'; + $expenses = $this->getExpenses(); + $income = $this->getIncome(); + $accountSummary = $this->getObjectSummary($this->summarizeByAccount($expenses), $this->summarizeByAccount($income)); + $categorySummary = $this->getObjectSummary($this->summarizeByCategory($expenses), $this->summarizeByCategory($income)); + $averageExpenses = $this->getAverages($expenses, SORT_ASC); + $averageIncome = $this->getAverages($income, SORT_DESC); + $topExpenses = $this->getTopExpenses(); + $topIncome = $this->getTopIncome(); + + + // render! + return view( + 'reports.category.month', + compact( + 'accountIds', 'categoryIds', 'topIncome', 'reportType', 'accountSummary', 'categorySummary', 'averageExpenses', 'averageIncome', 'topExpenses' + ) + ) + ->with('start', $this->start)->with('end', $this->end) + ->with('categories', $this->categories) + ->with('accounts', $this->accounts) + ->render(); + } + + /** + * @param Collection $accounts + * + * @return ReportGeneratorInterface + */ + public function setAccounts(Collection $accounts): ReportGeneratorInterface + { + $this->accounts = $accounts; + + return $this; + } + + /** + * @param Collection $budgets + * + * @return ReportGeneratorInterface + */ + public function setBudgets(Collection $budgets): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Collection $categories + * + * @return ReportGeneratorInterface + */ + public function setCategories(Collection $categories): ReportGeneratorInterface + { + $this->categories = $categories; + + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setEndDate(Carbon $date): ReportGeneratorInterface + { + $this->end = $date; + + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setStartDate(Carbon $date): ReportGeneratorInterface + { + $this->start = $date; + + return $this; + } + + /** + * @param Collection $tags + * + * @return ReportGeneratorInterface + */ + public function setTags(Collection $tags): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Collection $collection + * @param int $sortFlag + * + * @return array + */ + private function getAverages(Collection $collection, int $sortFlag): array + { + $result = []; + /** @var Transaction $transaction */ + foreach ($collection as $transaction) { + // opposing name and ID: + $opposingId = $transaction->opposing_account_id; + + // is not set? + if (!isset($result[$opposingId])) { + $name = $transaction->opposing_account_name; + $result[$opposingId] = [ + 'name' => $name, + 'count' => 1, + 'id' => $opposingId, + 'average' => $transaction->transaction_amount, + 'sum' => $transaction->transaction_amount, + ]; + continue; + } + $result[$opposingId]['count']++; + $result[$opposingId]['sum'] = bcadd($result[$opposingId]['sum'], $transaction->transaction_amount); + $result[$opposingId]['average'] = bcdiv($result[$opposingId]['sum'], strval($result[$opposingId]['count'])); + } + + // sort result by average: + $average = []; + foreach ($result as $key => $row) { + $average[$key] = floatval($row['average']); + } + + array_multisort($average, $sortFlag, $result); + + return $result; + } + + /** + * @return Collection + */ + private function getExpenses(): Collection + { + if ($this->expenses->count() > 0) { + Log::debug('Return previous set of expenses.'); + + return $this->expenses; + } + + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($this->accounts)->setRange($this->start, $this->end) + ->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) + ->setCategories($this->categories)->withOpposingAccount()->disableFilter(); + + $accountIds = $this->accounts->pluck('id')->toArray(); + $transactions = $collector->getJournals(); + $transactions = self::filterExpenses($transactions, $accountIds); + $this->expenses = $transactions; + + return $transactions; + } + + /** + * @return Collection + */ + private function getIncome(): Collection + { + if ($this->income->count() > 0) { + return $this->income; + } + + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($this->accounts)->setRange($this->start, $this->end) + ->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) + ->setCategories($this->categories)->withOpposingAccount(); + $accountIds = $this->accounts->pluck('id')->toArray(); + $transactions = $collector->getJournals(); + $transactions = self::filterIncome($transactions, $accountIds); + $this->income = $transactions; + + return $transactions; + } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. + * @param array $spent + * @param array $earned + * + * @return array + */ + private function getObjectSummary(array $spent, array $earned): array + { + $return = []; + + /** + * @var int $accountId + * @var string $entry + */ + foreach ($spent as $objectId => $entry) { + if (!isset($return[$objectId])) { + $return[$objectId] = ['spent' => 0, 'earned' => 0]; + } + + $return[$objectId]['spent'] = $entry; + } + unset($entry); + + /** + * @var int $accountId + * @var string $entry + */ + foreach ($earned as $objectId => $entry) { + if (!isset($return[$objectId])) { + $return[$objectId] = ['spent' => 0, 'earned' => 0]; + } + + $return[$objectId]['earned'] = $entry; + } + + + return $return; + } + + + /** + * @return Collection + */ + private function getTopExpenses(): Collection + { + $transactions = $this->getExpenses()->sortBy('transaction_amount'); + + return $transactions; + } + + /** + * @return Collection + */ + private function getTopIncome(): Collection + { + $transactions = $this->getIncome()->sortByDesc('transaction_amount'); + + return $transactions; + } + + /** + * @param Collection $collection + * + * @return array + */ + private function summarizeByAccount(Collection $collection): array + { + $result = []; + /** @var Transaction $transaction */ + foreach ($collection as $transaction) { + $accountId = $transaction->account_id; + $result[$accountId] = $result[$accountId] ?? '0'; + $result[$accountId] = bcadd($transaction->transaction_amount, $result[$accountId]); + } + + return $result; + } + + /** + * @param Collection $collection + * + * @return array + */ + private function summarizeByCategory(Collection $collection): array + { + $result = []; + /** @var Transaction $transaction */ + foreach ($collection as $transaction) { + $jrnlCatId = intval($transaction->transaction_journal_category_id); + $transCatId = intval($transaction->transaction_category_id); + $categoryId = max($jrnlCatId, $transCatId); + $result[$categoryId] = $result[$categoryId] ?? '0'; + $result[$categoryId] = bcadd($transaction->transaction_amount, $result[$categoryId]); + } + + return $result; + } +} diff --git a/app/Generator/Report/Category/MultiYearReportGenerator.php b/app/Generator/Report/Category/MultiYearReportGenerator.php new file mode 100644 index 0000000000..62b0e32af9 --- /dev/null +++ b/app/Generator/Report/Category/MultiYearReportGenerator.php @@ -0,0 +1,27 @@ +diffInMonths($end) > 1) { + $period = 'Year'; + } + + // more than one year date difference means multi year report. + if ($start->diffInMonths($end) > 12) { + $period = 'MultiYear'; + } + + + $class = sprintf('FireflyIII\Generator\Report\%s\%sReportGenerator', $type, $period); + if (class_exists($class)) { + /** @var ReportGeneratorInterface $obj */ + $obj = new $class; + $obj->setStartDate($start); + $obj->setEndDate($end); + + return $obj; + } + throw new FireflyException(sprintf('Cannot generate report. There is no "%s"-report for period "%s".', $type, $period)); + } +} diff --git a/app/Generator/Report/ReportGeneratorInterface.php b/app/Generator/Report/ReportGeneratorInterface.php new file mode 100644 index 0000000000..12942190f4 --- /dev/null +++ b/app/Generator/Report/ReportGeneratorInterface.php @@ -0,0 +1,74 @@ +getBillReport($this->start, $this->end, $this->accounts); + $accountIds = join(',', $this->accounts->pluck('id')->toArray()); + $reportType = 'default'; + + // continue! + return view( + 'reports.default.month', + compact('bills', 'accountIds', 'reportType') + )->with('start', $this->start)->with('end', $this->end)->render(); + } + + /** + * @param Collection $accounts + * + * @return ReportGeneratorInterface + */ + public function setAccounts(Collection $accounts): ReportGeneratorInterface + { + $this->accounts = $accounts; + + return $this; + } + + /** + * @param Collection $budgets + * + * @return ReportGeneratorInterface + */ + public function setBudgets(Collection $budgets): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Collection $categories + * + * @return ReportGeneratorInterface + */ + public function setCategories(Collection $categories): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setEndDate(Carbon $date): ReportGeneratorInterface + { + $this->end = $date; + + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setStartDate(Carbon $date): ReportGeneratorInterface + { + $this->start = $date; + + return $this; + } + + /** + * @param Collection $tags + * + * @return ReportGeneratorInterface + */ + public function setTags(Collection $tags): ReportGeneratorInterface + { + return $this; + } +} diff --git a/app/Generator/Report/Standard/MultiYearReportGenerator.php b/app/Generator/Report/Standard/MultiYearReportGenerator.php new file mode 100644 index 0000000000..a5b1702a95 --- /dev/null +++ b/app/Generator/Report/Standard/MultiYearReportGenerator.php @@ -0,0 +1,116 @@ +accounts->pluck('id')->toArray()); + $reportType = 'default'; + + // continue! + return view( + 'reports.default.multi-year', + compact('accountIds', 'reportType') + )->with('start', $this->start)->with('end', $this->end)->render(); + } + + /** + * @param Collection $accounts + * + * @return ReportGeneratorInterface + */ + public function setAccounts(Collection $accounts): ReportGeneratorInterface + { + $this->accounts = $accounts; + + return $this; + } + + /** + * @param Collection $budgets + * + * @return ReportGeneratorInterface + */ + public function setBudgets(Collection $budgets): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Collection $categories + * + * @return ReportGeneratorInterface + */ + public function setCategories(Collection $categories): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setEndDate(Carbon $date): ReportGeneratorInterface + { + $this->end = $date; + + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setStartDate(Carbon $date): ReportGeneratorInterface + { + $this->start = $date; + + return $this; + } + + /** + * @param Collection $tags + * + * @return ReportGeneratorInterface + */ + public function setTags(Collection $tags): ReportGeneratorInterface + { + return $this; + } +} diff --git a/app/Generator/Report/Standard/YearReportGenerator.php b/app/Generator/Report/Standard/YearReportGenerator.php new file mode 100644 index 0000000000..e755b45057 --- /dev/null +++ b/app/Generator/Report/Standard/YearReportGenerator.php @@ -0,0 +1,116 @@ +accounts->pluck('id')->toArray()); + $reportType = 'default'; + + // continue! + return view( + 'reports.default.year', + compact('accountIds', 'reportType') + )->with('start', $this->start)->with('end', $this->end)->render(); + } + + /** + * @param Collection $accounts + * + * @return ReportGeneratorInterface + */ + public function setAccounts(Collection $accounts): ReportGeneratorInterface + { + $this->accounts = $accounts; + + return $this; + } + + /** + * @param Collection $budgets + * + * @return ReportGeneratorInterface + */ + public function setBudgets(Collection $budgets): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Collection $categories + * + * @return ReportGeneratorInterface + */ + public function setCategories(Collection $categories): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setEndDate(Carbon $date): ReportGeneratorInterface + { + $this->end = $date; + + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setStartDate(Carbon $date): ReportGeneratorInterface + { + $this->start = $date; + + return $this; + } + + /** + * @param Collection $tags + * + * @return ReportGeneratorInterface + */ + public function setTags(Collection $tags): ReportGeneratorInterface + { + return $this; + } +} diff --git a/app/Generator/Report/Support.php b/app/Generator/Report/Support.php new file mode 100644 index 0000000000..573129ef97 --- /dev/null +++ b/app/Generator/Report/Support.php @@ -0,0 +1,83 @@ +filter( + function (Transaction $transaction) use ($accounts, $modifier) { + $opposing = $transaction->opposing_account_id; + // remove internal transfer + if (in_array($opposing, $accounts)) { + Log::debug(sprintf('Filtered #%d because its opposite is in accounts.', $transaction->id)); + + return null; + } + // remove positive amount + if (bccomp($transaction->transaction_amount, '0') === $modifier) { + Log::debug(sprintf('Filtered #%d because amount is %f.', $transaction->id, $transaction->transaction_amount)); + + return null; + } + + return $transaction; + } + ); + + return $result; + } + +} diff --git a/app/Handlers/Events/BudgetEventHandler.php b/app/Handlers/Events/BudgetEventHandler.php deleted file mode 100644 index 86491613f3..0000000000 --- a/app/Handlers/Events/BudgetEventHandler.php +++ /dev/null @@ -1,110 +0,0 @@ -budgetLimit; - $end = $event->end; - $set = $budgetLimit->limitrepetitions() - ->where('startdate', $budgetLimit->startdate->format('Y-m-d 00:00:00')) - ->where('enddate', $end->format('Y-m-d 00:00:00')) - ->get(); - if ($set->count() == 0) { - $repetition = new LimitRepetition; - $repetition->startdate = $budgetLimit->startdate; - $repetition->enddate = $end; - $repetition->amount = $budgetLimit->amount; - $repetition->budgetLimit()->associate($budgetLimit); - - try { - $repetition->save(); - } catch (QueryException $e) { - Log::error('Trying to save new LimitRepetition failed: ' . $e->getMessage()); - } - } - - if ($set->count() == 1) { - $repetition = $set->first(); - $repetition->amount = $budgetLimit->amount; - $repetition->save(); - - } - - return true; - } - - - /** - * Updates, if present the budget limit repetition part of a budget limit. - * - * @param UpdatedBudgetLimit $event - * - * @return bool - */ - public function updateRepetition(UpdatedBudgetLimit $event): bool - { - $budgetLimit = $event->budgetLimit; - $end = $event->end; - $set = $budgetLimit->limitrepetitions() - ->where('startdate', $budgetLimit->startdate->format('Y-m-d 00:00:00')) - ->where('enddate', $end->format('Y-m-d 00:00:00')) - ->get(); - if ($set->count() == 0) { - $repetition = new LimitRepetition; - $repetition->startdate = $budgetLimit->startdate; - $repetition->enddate = $end; - $repetition->amount = $budgetLimit->amount; - $repetition->budgetLimit()->associate($budgetLimit); - - try { - $repetition->save(); - } catch (QueryException $e) { - Log::error('Trying to save new LimitRepetition failed: ' . $e->getMessage()); - } - } - - if ($set->count() == 1) { - $repetition = $set->first(); - $repetition->amount = $budgetLimit->amount; - $repetition->save(); - - } - - return true; - } -} diff --git a/app/Handlers/Events/StoredJournalEventHandler.php b/app/Handlers/Events/StoredJournalEventHandler.php index 113c542869..14e4e2a438 100644 --- a/app/Handlers/Events/StoredJournalEventHandler.php +++ b/app/Handlers/Events/StoredJournalEventHandler.php @@ -16,11 +16,13 @@ namespace FireflyIII\Handlers\Events; use FireflyIII\Events\StoredTransactionJournal; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBankEvent; +use FireflyIII\Models\PiggyBankRepetition; use FireflyIII\Models\Rule; use FireflyIII\Models\RuleGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Rules\Processor; use FireflyIII\Support\Events\BillScanner; +use Log; /** * Class StoredJournalEventHandler @@ -41,31 +43,31 @@ class StoredJournalEventHandler /** @var TransactionJournal $journal */ $journal = $event->journal; $piggyBankId = $event->piggyBankId; + Log::debug(sprintf('Trying to connect journal %d to piggy bank %d.', $journal->id, $piggyBankId)); - /** @var PiggyBank $piggyBank */ - $piggyBank = $journal->user->piggyBanks()->where('piggy_banks.id', $piggyBankId)->first(['piggy_banks.*']); + /* + * Verify existence of piggy bank: + */ + if (!$this->verifyExistence($event)) { + Log::error(sprintf('No such piggy bank or no repetition on %s', $journal->date->format('Y-m-d'))); - if (is_null($piggyBank)) { - return true; - } - // update piggy bank rep for date of transaction journal. - $repetition = $piggyBank->piggyBankRepetitions()->relevantOnDate($journal->date)->first(); - if (is_null($repetition)) { return true; } - $amount = TransactionJournal::amountPositive($journal); - // if piggy account matches source account, the amount is positive - $sources = TransactionJournal::sourceAccountList($journal)->pluck('id')->toArray(); - if (in_array($piggyBank->account_id, $sources)) { - $amount = bcmul($amount, '-1'); - } - - + /* + * Get relevant data: + */ + $piggyBank = $journal->user->piggyBanks()->where('piggy_banks.id', $piggyBankId)->first(['piggy_banks.*']); + $repetition = $piggyBank->piggyBankRepetitions()->relevantOnDate($journal->date)->first(); + $amount = $this->getExactAmount($journal, $piggyBank, $repetition); $repetition->currentamount = bcadd($repetition->currentamount, $amount); $repetition->save(); - PiggyBankEvent::create(['piggy_bank_id' => $piggyBank->id, 'transaction_journal_id' => $journal->id, 'date' => $journal->date, 'amount' => $amount]); + /** @var PiggyBankEvent $event */ + $event = PiggyBankEvent::create( + ['piggy_bank_id' => $piggyBank->id, 'transaction_journal_id' => $journal->id, 'date' => $journal->date, 'amount' => $amount] + ); + Log::debug(sprintf('Created piggy bank event #%d', $event->id)); return true; } @@ -73,14 +75,14 @@ class StoredJournalEventHandler /** * This method grabs all the users rules and processes them. * - * @param StoredTransactionJournal $event + * @param StoredTransactionJournal $storedJournalEvent * * @return bool */ - public function processRules(StoredTransactionJournal $event): bool + public function processRules(StoredTransactionJournal $storedJournalEvent): bool { // get all the user's rule groups, with the rules, order by 'order'. - $journal = $event->journal; + $journal = $storedJournalEvent->journal; $groups = $journal->user->ruleGroups()->where('rule_groups.active', 1)->orderBy('order', 'ASC')->get(); // /** @var RuleGroup $group */ @@ -110,15 +112,92 @@ class StoredJournalEventHandler /** * This method calls a special bill scanner that will check if the stored journal is part of a bill. * - * @param StoredTransactionJournal $event + * @param StoredTransactionJournal $storedJournalEvent * * @return bool */ - public function scanBills(StoredTransactionJournal $event): bool + public function scanBills(StoredTransactionJournal $storedJournalEvent): bool { - $journal = $event->journal; + $journal = $storedJournalEvent->journal; BillScanner::scan($journal); return true; } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's 6 but I can live with it. + * @param TransactionJournal $journal + * @param PiggyBank $piggyBank + * @param PiggyBankRepetition $repetition + * + * @return string + */ + private function getExactAmount(TransactionJournal $journal, PiggyBank $piggyBank, PiggyBankRepetition $repetition): string + { + $amount = TransactionJournal::amountPositive($journal); + $sources = TransactionJournal::sourceAccountList($journal)->pluck('id')->toArray(); + $room = bcsub(strval($piggyBank->targetamount), strval($repetition->currentamount)); + $compare = bcmul($repetition->currentamount, '-1'); + + Log::debug(sprintf('Will add/remove %f to piggy bank #%d ("%s")', $amount, $piggyBank->id, $piggyBank->name)); + + // if piggy account matches source account, the amount is positive + if (in_array($piggyBank->account_id, $sources)) { + $amount = bcmul($amount, '-1'); + Log::debug(sprintf('Account #%d is the source, so will remove amount from piggy bank.', $piggyBank->account_id)); + } + + + // if the amount is positive, make sure it fits in piggy bank: + if (bccomp($amount, '0') === 1 && bccomp($room, $amount) === -1) { + // amount is positive and $room is smaller than $amount + Log::debug(sprintf('Room in piggy bank for extra money is %f', $room)); + Log::debug(sprintf('There is NO room to add %f to piggy bank #%d ("%s")', $amount, $piggyBank->id, $piggyBank->name)); + Log::debug(sprintf('New amount is %f', $room)); + + return $room; + } + + // amount is negative and $currentamount is smaller than $amount + if (bccomp($amount, '0') === -1 && bccomp($compare, $amount) === 1) { + Log::debug(sprintf('Max amount to remove is %f', $repetition->currentamount)); + Log::debug(sprintf('Cannot remove %f from piggy bank #%d ("%s")', $amount, $piggyBank->id, $piggyBank->name)); + Log::debug(sprintf('New amount is %f', $compare)); + + return $compare; + } + + return $amount; + } + + /** + * @param StoredTransactionJournal $event + * + * @return bool + */ + private function verifyExistence(StoredTransactionJournal $event): bool + { + /** @var TransactionJournal $journal */ + $journal = $event->journal; + $piggyBankId = $event->piggyBankId; + + /** @var PiggyBank $piggyBank */ + $piggyBank = $journal->user->piggyBanks()->where('piggy_banks.id', $piggyBankId)->first(['piggy_banks.*']); + + if (is_null($piggyBank)) { + Log::error('No such piggy bank!'); + + return false; + } + Log::debug(sprintf('Found piggy bank #%d: "%s"', $piggyBank->id, $piggyBank->name)); + // update piggy bank rep for date of transaction journal. + $repetition = $piggyBank->piggyBankRepetitions()->relevantOnDate($journal->date)->first(); + if (is_null($repetition)) { + Log::error(sprintf('No piggy bank repetition on %s!', $journal->date->format('Y-m-d'))); + + return false; + } + + return true; + } } diff --git a/app/Handlers/Events/UpdatedJournalEventHandler.php b/app/Handlers/Events/UpdatedJournalEventHandler.php index 8dbd8ae5f3..0deea00280 100644 --- a/app/Handlers/Events/UpdatedJournalEventHandler.php +++ b/app/Handlers/Events/UpdatedJournalEventHandler.php @@ -15,11 +15,8 @@ namespace FireflyIII\Handlers\Events; use FireflyIII\Events\UpdatedTransactionJournal; -use FireflyIII\Models\PiggyBankEvent; -use FireflyIII\Models\PiggyBankRepetition; use FireflyIII\Models\Rule; use FireflyIII\Models\RuleGroup; -use FireflyIII\Models\TransactionJournal; use FireflyIII\Rules\Processor; use FireflyIII\Support\Events\BillScanner; @@ -30,62 +27,18 @@ use FireflyIII\Support\Events\BillScanner; */ class UpdatedJournalEventHandler { - /** - * This method will try to reconnect a journal to a piggy bank, updating the piggy bank repetition. - * - * @param UpdatedTransactionJournal $event - * - * @return bool - */ - public function connectToPiggyBank(UpdatedTransactionJournal $event): bool - { - $journal = $event->journal; - - if (!$journal->isTransfer()) { - return true; - } - - // get the event connected to this journal: - /** @var PiggyBankEvent $event */ - $event = PiggyBankEvent::where('transaction_journal_id', $journal->id)->first(); - if (is_null($event)) { - return false; - } - $piggyBank = $event->piggyBank()->first(); - $repetition = null; - if (!is_null($piggyBank)) { - /** @var PiggyBankRepetition $repetition */ - $repetition = $piggyBank->piggyBankRepetitions()->relevantOnDate($journal->date)->first(); - } - - if (is_null($repetition)) { - return false; - } - - $amount = TransactionJournal::amount($journal); - $diff = bcsub($amount, $event->amount); // update current repetition - - $repetition->currentamount = bcadd($repetition->currentamount, $diff); - $repetition->save(); - - - $event->amount = $amount; - $event->save(); - - return true; - } /** * This method will check all the rules when a journal is updated. * - * @param UpdatedTransactionJournal $event + * @param UpdatedTransactionJournal $updatedJournalEvent * * @return bool */ - public function processRules(UpdatedTransactionJournal $event):bool + public function processRules(UpdatedTransactionJournal $updatedJournalEvent): bool { // get all the user's rule groups, with the rules, order by 'order'. - $journal = $event->journal; + $journal = $updatedJournalEvent->journal; $groups = $journal->user->ruleGroups()->where('rule_groups.active', 1)->orderBy('order', 'ASC')->get(); // /** @var RuleGroup $group */ @@ -114,13 +67,13 @@ class UpdatedJournalEventHandler /** * This method calls a special bill scanner that will check if the updated journal is part of a bill. * - * @param UpdatedTransactionJournal $event + * @param UpdatedTransactionJournal $updatedJournalEvent * * @return bool */ - public function scanBills(UpdatedTransactionJournal $event): bool + public function scanBills(UpdatedTransactionJournal $updatedJournalEvent): bool { - $journal = $event->journal; + $journal = $updatedJournalEvent->journal; BillScanner::scan($journal); return true; diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index 4d9651158f..5144843bf7 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -13,15 +13,12 @@ declare(strict_types = 1); namespace FireflyIII\Handlers\Events; -use Exception; -use FireflyIII\Events\ConfirmedUser; use FireflyIII\Events\RegisteredUser; -use FireflyIII\Events\ResentConfirmation; +use FireflyIII\Events\RequestedNewPassword; use FireflyIII\Repositories\User\UserRepositoryInterface; use Illuminate\Mail\Message; use Log; use Mail; -use Preferences; use Session; use Swift_TransportException; @@ -32,7 +29,6 @@ use Swift_TransportException; * * The method name reflects what is being done. This is in the present tense. * - * * @package FireflyIII\Handlers\Events */ class UserEventHandler @@ -73,90 +69,32 @@ class UserEventHandler } /** - * This method will send a newly registered user a confirmation message, urging him or her to activate their account. - * - * @param RegisteredUser $event + * @param RequestedNewPassword $event * * @return bool */ - public function sendConfirmationMessage(RegisteredUser $event): bool + public function sendNewPassword(RequestedNewPassword $event): bool { - $user = $event->user; - $ipAddress = $event->ipAddress; - $confirmAccount = env('MUST_CONFIRM_ACCOUNT', false); - if ($confirmAccount === false) { - Preferences::setForUser($user, 'user_confirmed', true); - Preferences::setForUser($user, 'user_confirmed_last_mail', 0); - Preferences::mark(); + $email = $event->user->email; + $ipAddress = $event->ipAddress; + $token = $event->token; - return true; - } - $email = $user->email; - $code = str_random(16); - $route = route('do_confirm_account', [$code]); - Preferences::setForUser($user, 'user_confirmed', false); - Preferences::setForUser($user, 'user_confirmed_last_mail', time()); - Preferences::setForUser($user, 'user_confirmed_code', $code); + $url = route('password.reset', [$token]); + + // send email. try { Mail::send( - ['emails.confirm-account-html', 'emails.confirm-account'], ['route' => $route, 'ip' => $ipAddress], - function (Message $message) use ($email) { - $message->to($email, $email)->subject('Please confirm your Firefly III account'); - } + ['emails.password-html', 'emails.password-text'], ['url' => $url, 'ip' => $ipAddress], function (Message $message) use ($email) { + $message->to($email, $email)->subject('Your password reset request'); + } ); } catch (Swift_TransportException $e) { Log::error($e->getMessage()); - } catch (Exception $e) { - Log::error($e->getMessage()); } return true; } - /** - * If the user has somehow lost his or her confirmation message, this event will send it to the user again. - * - * At the moment, this method is exactly the same as the ::sendConfirmationMessage method, but that will change. - * - * @param ResentConfirmation $event - * - * @return bool - */ - function sendConfirmationMessageAgain(ResentConfirmation $event): bool - { - $user = $event->user; - $ipAddress = $event->ipAddress; - $confirmAccount = env('MUST_CONFIRM_ACCOUNT', false); - if ($confirmAccount === false) { - Preferences::setForUser($user, 'user_confirmed', true); - Preferences::setForUser($user, 'user_confirmed_last_mail', 0); - Preferences::mark(); - - return true; - } - $email = $user->email; - $code = str_random(16); - $route = route('do_confirm_account', [$code]); - Preferences::setForUser($user, 'user_confirmed', false); - Preferences::setForUser($user, 'user_confirmed_last_mail', time()); - Preferences::setForUser($user, 'user_confirmed_code', $code); - try { - Mail::send( - ['emails.confirm-account-html', 'emails.confirm-account'], ['route' => $route, 'ip' => $ipAddress], - function (Message $message) use ($email) { - $message->to($email, $email)->subject('Please confirm your Firefly III account'); - } - ); - } catch (Swift_TransportException $e) { - Log::error($e->getMessage()); - } catch (Exception $e) { - Log::error($e->getMessage()); - } - - return true; - - } - /** * This method will send the user a registration mail, welcoming him or her to Firefly III. * This message is only sent when the configuration of Firefly III says so. @@ -179,8 +117,8 @@ class UserEventHandler // send email. try { Mail::send( - ['emails.registered-html', 'emails.registered'], ['address' => $address, 'ip' => $ipAddress], function (Message $message) use ($email) { - $message->to($email, $email)->subject('Welcome to Firefly III! '); + ['emails.registered-html', 'emails.registered-text'], ['address' => $address, 'ip' => $ipAddress], function (Message $message) use ($email) { + $message->to($email, $email)->subject('Welcome to Firefly III!'); } ); } catch (Swift_TransportException $e) { @@ -189,37 +127,4 @@ class UserEventHandler return true; } - - /** - * When the user is confirmed, this method stores the IP address of the user - * as a preference. Since this preference cannot be edited, it is effectively hidden - * from the user yet stored conveniently. - * - * @param ConfirmedUser $event - * - * @return bool - */ - public function storeConfirmationIpAddress(ConfirmedUser $event): bool - { - Preferences::setForUser($event->user, 'confirmation_ip_address', $event->ipAddress); - - return true; - } - - /** - * This message stores the users IP address on registration, in much the same - * fashion as the previous method. - * - * @param RegisteredUser $event - * - * @return bool - */ - public function storeRegistrationIpAddress(RegisteredUser $event): bool - { - Preferences::setForUser($event->user, 'registration_ip_address', $event->ipAddress); - - return true; - - } - } diff --git a/app/Helpers/Attachments/AttachmentHelper.php b/app/Helpers/Attachments/AttachmentHelper.php index a7bb7c8573..c7f7902109 100644 --- a/app/Helpers/Attachments/AttachmentHelper.php +++ b/app/Helpers/Attachments/AttachmentHelper.php @@ -16,11 +16,8 @@ use Crypt; use FireflyIII\Models\Attachment; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\MessageBag; -use Input; -use Log; use Storage; use Symfony\Component\HttpFoundation\File\UploadedFile; -use TypeError; /** * Class AttachmentHelper @@ -35,9 +32,9 @@ class AttachmentHelper implements AttachmentHelperInterface /** @var MessageBag */ public $messages; /** @var array */ - protected $allowedMimes; + protected $allowedMimes = []; /** @var int */ - protected $maxUploadSize; + protected $maxUploadSize = 0; /** @var \Illuminate\Contracts\Filesystem\Filesystem */ protected $uploadDisk; @@ -47,8 +44,8 @@ class AttachmentHelper implements AttachmentHelperInterface */ public function __construct() { - $this->maxUploadSize = config('firefly.maxUploadSize'); - $this->allowedMimes = config('firefly.allowedMimes'); + $this->maxUploadSize = intval(config('firefly.maxUploadSize')); + $this->allowedMimes = (array) config('firefly.allowedMimes'); $this->errors = new MessageBag; $this->messages = new MessageBag; $this->uploadDisk = Storage::disk('upload'); @@ -83,20 +80,19 @@ class AttachmentHelper implements AttachmentHelperInterface } /** - * @param Model $model + * @param Model $model + * @param array|null $files * * @return bool */ - public function saveAttachmentsForModel(Model $model): bool + public function saveAttachmentsForModel(Model $model, array $files = null): bool { - $files = $this->getFiles(); - - if (!is_null($files) && !is_array($files)) { - $this->processFile($files, $model); - } - if (is_array($files)) { - $this->processFiles($files, $model); + foreach ($files as $entry) { + if (!is_null($entry)) { + $this->processFile($entry, $model); + } + } } return true; @@ -229,42 +225,4 @@ class AttachmentHelper implements AttachmentHelperInterface return true; } - - /** - * @return array|null|UploadedFile - */ - private function getFiles() - { - $files = null; - try { - if (Input::hasFile('attachments')) { - $files = Input::file('attachments'); - } - } catch (TypeError $e) { - // Log it, do nothing else. - Log::error($e->getMessage()); - } - - return $files; - } - - /** - * @param array $files - * - * @param Model $model - * - * @return bool - */ - private function processFiles(array $files, Model $model): bool - { - foreach ($files as $entry) { - if (!is_null($entry)) { - $this->processFile($entry, $model); - } - } - - return true; - } - - } diff --git a/app/Helpers/Attachments/AttachmentHelperInterface.php b/app/Helpers/Attachments/AttachmentHelperInterface.php index ba98fd07db..dc6f9420aa 100644 --- a/app/Helpers/Attachments/AttachmentHelperInterface.php +++ b/app/Helpers/Attachments/AttachmentHelperInterface.php @@ -46,6 +46,6 @@ interface AttachmentHelperInterface * * @return bool */ - public function saveAttachmentsForModel(Model $model): bool; + public function saveAttachmentsForModel(Model $model, array $files = null): bool; } diff --git a/app/Helpers/Chart/MetaPieChart.php b/app/Helpers/Chart/MetaPieChart.php new file mode 100644 index 0000000000..6c340b5389 --- /dev/null +++ b/app/Helpers/Chart/MetaPieChart.php @@ -0,0 +1,277 @@ + ['opposing_account_id'], + 'budget' => ['transaction_journal_budget_id', 'transaction_budget_id'], + 'category' => ['transaction_journal_category_id', 'transaction_category_id'], + ]; + + /** @var array */ + protected $repositories + = [ + 'account' => AccountRepositoryInterface::class, + 'budget' => BudgetRepositoryInterface::class, + 'category' => CategoryRepositoryInterface::class, + ]; + + + /** @var Carbon */ + protected $start; + /** @var string */ + protected $total = '0'; + /** @var User */ + protected $user; + + public function __construct() + { + $this->accounts = new Collection; + $this->budgets = new Collection; + $this->categories = new Collection; + } + + /** + * @param string $direction + * @param string $group + * + * @return array + */ + public function generate(string $direction, string $group): array + { + $transactions = $this->getTransactions($direction); + $grouped = $this->groupByFields($transactions, $this->grouping[$group]); + $chartData = $this->organizeByType($group, $grouped); + + // also collect all other transactions + if ($this->collectOtherObjects && $direction === 'expense') { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setAccounts($this->accounts)->setRange($this->start, $this->end)->setTypes([TransactionType::WITHDRAWAL]); + $journals = $collector->getJournals(); + $sum = strval($journals->sum('transaction_amount')); + $sum = bcmul($sum, '-1'); + $sum = bcsub($sum, $this->total); + $chartData[strval(trans('firefly.everything_else'))] = $sum; + } + + if ($this->collectOtherObjects && $direction === 'income') { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($this->accounts)->setRange($this->start, $this->end)->setTypes([TransactionType::DEPOSIT]); + $journals = $collector->getJournals(); + $sum = strval($journals->sum('transaction_amount')); + $sum = bcsub($sum, $this->total); + $chartData[strval(trans('firefly.everything_else'))] = $sum; + } + + return $chartData; + + } + + /** + * @param Collection $accounts + * + * @return MetaPieChartInterface + */ + public function setAccounts(Collection $accounts): MetaPieChartInterface + { + $this->accounts = $accounts; + + return $this; + } + + /** + * @param Collection $budgets + * + * @return MetaPieChartInterface + */ + public function setBudgets(Collection $budgets): MetaPieChartInterface + { + $this->budgets = $budgets; + + return $this; + } + + /** + * @param Collection $categories + * + * @return MetaPieChartInterface + */ + public function setCategories(Collection $categories): MetaPieChartInterface + { + $this->categories = $categories; + + return $this; + } + + /** + * @param bool $collectOtherObjects + * + * @return MetaPieChartInterface + */ + public function setCollectOtherObjects(bool $collectOtherObjects): MetaPieChartInterface + { + $this->collectOtherObjects = $collectOtherObjects; + + return $this; + } + + /** + * @param Carbon $end + * + * @return MetaPieChartInterface + */ + public function setEnd(Carbon $end): MetaPieChartInterface + { + $this->end = $end; + + return $this; + } + + /** + * @param Carbon $start + * + * @return MetaPieChartInterface + */ + public function setStart(Carbon $start): MetaPieChartInterface + { + $this->start = $start; + + return $this; + } + + /** + * @param User $user + * + * @return MetaPieChartInterface + */ + public function setUser(User $user): MetaPieChartInterface + { + $this->user = $user; + + return $this; + } + + protected function getTransactions(string $direction) + { + $types = [TransactionType::DEPOSIT, TransactionType::TRANSFER]; + $modifier = -1; + if ($direction === 'expense') { + $types = [TransactionType::WITHDRAWAL, TransactionType::TRANSFER]; + $modifier = 1; + } + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($this->accounts); + $collector->setRange($this->start, $this->end); + $collector->setTypes($types); + $collector->withOpposingAccount(); + + if ($direction === 'income') { + $collector->disableFilter(); + } + + if ($this->budgets->count() > 0) { + $collector->setBudgets($this->budgets); + } + if ($this->categories->count() > 0) { + $collector->setCategories($this->categories); + } + + $accountIds = $this->accounts->pluck('id')->toArray(); + $transactions = $collector->getJournals(); + $set = Support::filterTransactions($transactions, $accountIds, $modifier); + + return $set; + } + + /** + * @param Collection $set + * @param array $fields + * + * @return array + */ + protected function groupByFields(Collection $set, array $fields) + { + $grouped = []; + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $values = []; + foreach ($fields as $field) { + $values[] = intval($transaction->$field); + } + $value = max($values); + $grouped[$value] = $grouped[$value] ?? '0'; + $grouped[$value] = bcadd($transaction->transaction_amount, $grouped[$value]); + } + + return $grouped; + } + + /** + * @param string $type + * @param array $array + * + * @return array + */ + protected function organizeByType(string $type, array $array): array + { + $chartData = []; + $names = []; + $repository = app($this->repositories[$type]); + foreach ($array as $objectId => $amount) { + if (!isset($names[$objectId])) { + $object = $repository->find(intval($objectId)); + $names[$objectId] = $object->name; + } + $amount = Steam::positive($amount); + $this->total = bcadd($this->total, $amount); + $chartData[$names[$objectId]] = $amount; + } + + return $chartData; + + } +} diff --git a/app/Helpers/Chart/MetaPieChartInterface.php b/app/Helpers/Chart/MetaPieChartInterface.php new file mode 100644 index 0000000000..006300d308 --- /dev/null +++ b/app/Helpers/Chart/MetaPieChartInterface.php @@ -0,0 +1,82 @@ +accounts = new Collection; - } - - /** - * @return Collection - */ - public function getAccounts(): Collection - { - return $this->accounts; - } - - /** - * @param Collection $accounts - */ - public function setAccounts(Collection $accounts) - { - $this->accounts = $accounts; - } - - /** - * @return string - */ - public function getDifference(): string - { - return $this->difference; - } - - /** - * @param string $difference - */ - public function setDifference(string $difference) - { - $this->difference = $difference; - } - - /** - * @return string - */ - public function getEnd(): string - { - return $this->end; - } - - /** - * @param string $end - */ - public function setEnd(string $end) - { - $this->end = $end; - } - - /** - * @return string - */ - public function getStart(): string - { - return $this->start; - } - - /** - * @param string $start - */ - public function setStart(string $start) - { - $this->start = $start; - } - - -} diff --git a/app/Helpers/Collection/BalanceLine.php b/app/Helpers/Collection/BalanceLine.php index 28b0d879ba..9b14902d40 100644 --- a/app/Helpers/Collection/BalanceLine.php +++ b/app/Helpers/Collection/BalanceLine.php @@ -14,6 +14,7 @@ namespace FireflyIII\Helpers\Collection; use Carbon\Carbon; use FireflyIII\Models\Budget as BudgetModel; +use FireflyIII\Models\BudgetLimit; use Illuminate\Support\Collection; /** @@ -34,12 +35,10 @@ class BalanceLine /** @var BudgetModel */ protected $budget; - /** @var Carbon */ - protected $endDate; + /** @var BudgetLimit */ + protected $budgetLimit; /** @var int */ protected $role = self::ROLE_DEFAULTROLE; - /** @var Carbon */ - protected $startDate; /** * @@ -90,20 +89,28 @@ class BalanceLine $this->budget = $budget; } + /** + * @return BudgetLimit + */ + public function getBudgetLimit(): BudgetLimit + { + return $this->budgetLimit; + } + + /** + * @param BudgetLimit $budgetLimit + */ + public function setBudgetLimit(BudgetLimit $budgetLimit) + { + $this->budgetLimit = $budgetLimit; + } + /** * @return Carbon */ public function getEndDate() { - return $this->endDate; - } - - /** - * @param Carbon $endDate - */ - public function setEndDate($endDate) - { - $this->endDate = $endDate; + return $this->budgetLimit->end_date ?? new Carbon; } /** @@ -127,18 +134,11 @@ class BalanceLine */ public function getStartDate() { - return $this->startDate; - } - - /** - * @param Carbon $startDate - */ - public function setStartDate($startDate) - { - $this->startDate = $startDate; + return $this->budgetLimit->start_date ?? new Carbon; } /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @return string */ public function getTitle(): string @@ -147,13 +147,13 @@ class BalanceLine return $this->getBudget()->name; } if ($this->getRole() == self::ROLE_DEFAULTROLE) { - return trans('firefly.no_budget'); + return strval(trans('firefly.no_budget')); } if ($this->getRole() == self::ROLE_TAGROLE) { - return trans('firefly.coveredWithTags'); + return strval(trans('firefly.coveredWithTags')); } if ($this->getRole() == self::ROLE_DIFFROLE) { - return trans('firefly.leftUnbalanced'); + return strval(trans('firefly.leftUnbalanced')); } return ''; @@ -169,7 +169,7 @@ class BalanceLine */ public function leftOfRepetition(): string { - $start = $this->budget->amount ?? '0'; + $start = $this->budgetLimit->amount ?? '0'; /** @var BalanceEntry $balanceEntry */ foreach ($this->getBalanceEntries() as $balanceEntry) { $start = bcadd($balanceEntry->getSpent(), $start); diff --git a/app/Helpers/Collection/Bill.php b/app/Helpers/Collection/Bill.php index ce83c87551..1cfac94d20 100644 --- a/app/Helpers/Collection/Bill.php +++ b/app/Helpers/Collection/Bill.php @@ -13,7 +13,10 @@ declare(strict_types = 1); namespace FireflyIII\Helpers\Collection; +use Carbon\Carbon; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; use Illuminate\Support\Collection; +use Log; /** * Class Bill @@ -26,7 +29,11 @@ class Bill /** * @var Collection */ - protected $bills; + private $bills; + /** @var Carbon */ + private $endDate; + /** @var Carbon */ + private $startDate; /** * @@ -44,6 +51,43 @@ class Bill $this->bills->push($bill); } + /** + * + */ + public function filterBills() + { + Log::debug('Now in filterBills()'); + /** @var BillRepositoryInterface $repository */ + $repository = app(BillRepositoryInterface::class); + $start = $this->startDate; + $end = $this->endDate; + $lines = $this->bills->filter( + function (BillLine $line) use ($repository, $start, $end) { + // next expected match? + $date = $start; + Log::debug(sprintf('Now at bill line for bill "%s"', $line->getBill()->name)); + Log::debug(sprintf('Default date to use is start date: %s', $date->format('Y-m-d'))); + if ($line->isHit()) { + $date = $line->getLastHitDate(); + Log::debug(sprintf('Line was hit, see date: %s. Always include it.', $date->format('Y-m-d'))); + + return $line; + } + $expected = $repository->nextExpectedMatch($line->getBill(), $date); + Log::debug(sprintf('Next expected match is %s', $expected->format('Y-m-d'))); + if ($expected <= $end && $expected >= $start) { + Log::debug('This date is inside report limits'); + + return $line; + } + Log::debug('This date is OUTSIDE report limits'); + + return false; + } + ); + $this->bills = $lines; + } + /** * @return Collection */ @@ -62,4 +106,20 @@ class Bill return $set; } + /** + * @param Carbon $endDate + */ + public function setEndDate(Carbon $endDate) + { + $this->endDate = $endDate; + } + + /** + * @param Carbon $startDate + */ + public function setStartDate(Carbon $startDate) + { + $this->startDate = $startDate; + } + } diff --git a/app/Helpers/Collection/BillLine.php b/app/Helpers/Collection/BillLine.php index 07e49bc30b..3b7bd692b6 100644 --- a/app/Helpers/Collection/BillLine.php +++ b/app/Helpers/Collection/BillLine.php @@ -12,6 +12,7 @@ declare(strict_types = 1); namespace FireflyIII\Helpers\Collection; +use Carbon\Carbon; use FireflyIII\Models\Bill as BillModel; /** @@ -23,8 +24,6 @@ use FireflyIII\Models\Bill as BillModel; class BillLine { - /** @var bool */ - protected $active; /** @var string */ protected $amount; /** @var BillModel */ @@ -35,10 +34,19 @@ class BillLine protected $max; /** @var string */ protected $min; - + /** @var Carbon */ + private $lastHitDate; /** @var int */ private $transactionJournalId; + /** + * BillLine constructor. + */ + public function __construct() + { + $this->lastHitDate = new Carbon; + } + /** * @return string */ @@ -71,6 +79,22 @@ class BillLine $this->bill = $bill; } + /** + * @return Carbon + */ + public function getLastHitDate(): Carbon + { + return $this->lastHitDate; + } + + /** + * @param Carbon $lastHitDate + */ + public function setLastHitDate(Carbon $lastHitDate) + { + $this->lastHitDate = $lastHitDate; + } + /** * @return string */ @@ -124,15 +148,7 @@ class BillLine */ public function isActive(): bool { - return $this->active; - } - - /** - * @param bool $active - */ - public function setActive(bool $active) - { - $this->active = $active; + return intval($this->bill->active) === 1; } /** @@ -151,4 +167,5 @@ class BillLine $this->hit = $hit; } + } diff --git a/app/Helpers/Collection/Budget.php b/app/Helpers/Collection/Budget.php deleted file mode 100644 index 8821940b3e..0000000000 --- a/app/Helpers/Collection/Budget.php +++ /dev/null @@ -1,197 +0,0 @@ -budgetLines = new Collection; - } - - /** - * @param BudgetLine $budgetLine - * - * @return Budget - */ - public function addBudgetLine(BudgetLine $budgetLine): Budget - { - $this->budgetLines->push($budgetLine); - - return $this; - } - - /** - * @param string $add - * - * @return Budget - */ - public function addBudgeted(string $add): Budget - { - $add = strval(round($add, 2)); - $this->budgeted = bcadd($this->budgeted, $add); - - return $this; - } - - /** - * @param string $add - * - * @return Budget - */ - public function addLeft(string $add): Budget - { - $add = strval(round($add, 2)); - $this->left = bcadd($this->left, $add); - - return $this; - } - - /** - * @param string $add - * - * @return Budget - */ - public function addOverspent(string $add): Budget - { - $add = strval(round($add, 2)); - $this->overspent = bcadd($this->overspent, $add); - - return $this; - } - - /** - * @param string $add - * - * @return Budget - */ - public function addSpent(string $add): Budget - { - $add = strval(round($add, 2)); - $this->spent = bcadd($this->spent, $add); - - return $this; - } - - /** - * @return \Illuminate\Support\Collection - */ - public function getBudgetLines(): Collection - { - return $this->budgetLines; - } - - /** - * @return string - */ - public function getBudgeted(): string - { - return $this->budgeted; - } - - /** - * @param string $budgeted - * - * @return Budget - */ - public function setBudgeted(string $budgeted): Budget - { - $this->budgeted = $budgeted; - - return $this; - } - - /** - * @return string - */ - public function getLeft(): string - { - return $this->left; - } - - /** - * @param string $left - * - * @return Budget - */ - public function setLeft(string $left): Budget - { - $this->left = $left; - - return $this; - } - - /** - * @return string - */ - public function getOverspent(): string - { - return $this->overspent; - } - - /** - * @param string $overspent - * - * @return Budget - */ - public function setOverspent(string $overspent): Budget - { - $this->overspent = strval(round($overspent, 2)); - - return $this; - } - - /** - * @return string - */ - public function getSpent(): string - { - return $this->spent; - } - - /** - * @param string $spent - * - * @return Budget - */ - public function setSpent(string $spent): Budget - { - $this->spent = strval(round($spent, 2)); - - return $this; - } - - -} diff --git a/app/Helpers/Collection/BudgetLine.php b/app/Helpers/Collection/BudgetLine.php deleted file mode 100644 index 5538c9e8f1..0000000000 --- a/app/Helpers/Collection/BudgetLine.php +++ /dev/null @@ -1,161 +0,0 @@ -budget ?? new BudgetModel; - } - - /** - * @param BudgetModel $budget - * - * @return BudgetLine - */ - public function setBudget(BudgetModel $budget): BudgetLine - { - $this->budget = $budget; - - return $this; - } - - /** - * @return string - */ - public function getBudgeted(): string - { - return $this->budgeted; - } - - /** - * @param string $budgeted - * - * @return BudgetLine - */ - public function setBudgeted(string $budgeted): BudgetLine - { - $this->budgeted = $budgeted; - - return $this; - } - - /** - * @return string - */ - public function getLeft(): string - { - return $this->left; - } - - /** - * @param string $left - * - * @return BudgetLine - */ - public function setLeft(string $left): BudgetLine - { - $this->left = $left; - - return $this; - } - - /** - * @return string - */ - public function getOverspent(): string - { - return $this->overspent; - } - - /** - * @param string $overspent - * - * @return BudgetLine - */ - public function setOverspent(string $overspent): BudgetLine - { - $this->overspent = $overspent; - - return $this; - } - - /** - * @return LimitRepetition - */ - public function getRepetition(): LimitRepetition - { - return $this->repetition ?? new LimitRepetition; - } - - /** - * @param LimitRepetition $repetition - * - * @return BudgetLine - */ - public function setRepetition(LimitRepetition $repetition): BudgetLine - { - $this->repetition = $repetition; - - return $this; - } - - /** - * @return string - */ - public function getSpent(): string - { - return $this->spent; - } - - /** - * @param string $spent - * - * @return BudgetLine - */ - public function setSpent(string $spent): BudgetLine - { - $this->spent = $spent; - - return $this; - } - - -} diff --git a/app/Helpers/Collection/Category.php b/app/Helpers/Collection/Category.php index 7090d387ba..faa4119a86 100644 --- a/app/Helpers/Collection/Category.php +++ b/app/Helpers/Collection/Category.php @@ -55,7 +55,6 @@ class Category */ public function addTotal(string $add) { - $add = strval(round($add, 2)); $this->total = bcadd($this->total, $add); } @@ -79,7 +78,7 @@ class Category */ public function getTotal(): string { - return strval(round($this->total, 2)); + return $this->total; } diff --git a/app/Helpers/Collection/Expense.php b/app/Helpers/Collection/Expense.php deleted file mode 100644 index 7a06e4f628..0000000000 --- a/app/Helpers/Collection/Expense.php +++ /dev/null @@ -1,85 +0,0 @@ -expenses = new Collection; - } - - /** - * @param stdClass $entry - */ - public function addOrCreateExpense(stdClass $entry) - { - $this->expenses->put($entry->id, $entry); - } - - /** - * @param string $add - */ - public function addToTotal(string $add) - { - $add = strval(round($add, 2)); - if (bccomp('0', $add) === -1) { - $add = bcmul($add, '-1'); - } - - // if amount is positive, the original transaction - // was a transfer. But since this is an expense report, - // that amount must be negative. - - $this->total = bcadd($this->total, $add); - } - - /** - * @return Collection - */ - public function getExpenses(): Collection - { - $set = $this->expenses->sortBy( - function (stdClass $object) { - return $object->amount; - } - ); - - return $set; - } - - /** - * @return string - */ - public function getTotal(): string - { - return strval(round($this->total, 2)); - } -} diff --git a/app/Helpers/Collection/Income.php b/app/Helpers/Collection/Income.php deleted file mode 100644 index 1bd6036150..0000000000 --- a/app/Helpers/Collection/Income.php +++ /dev/null @@ -1,81 +0,0 @@ -incomes = new Collection; - } - - /** - * @param stdClass $entry - */ - public function addOrCreateIncome(stdClass $entry) - { - $this->incomes->put($entry->id, $entry); - - } - - /** - * @param string $add - */ - public function addToTotal(string $add) - { - $add = strval(round($add, 2)); - $this->total = bcadd($this->total, $add); - } - - /** - * @return Collection - */ - public function getIncomes(): Collection - { - $set = $this->incomes->sortByDesc( - function (stdClass $object) { - return $object->amount; - } - ); - - return $set; - } - - /** - * @return string - */ - public function getTotal(): string - { - return strval(round($this->total, 2)); - } - - -} diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/JournalCollector.php new file mode 100644 index 0000000000..f709eff9ba --- /dev/null +++ b/app/Helpers/Collector/JournalCollector.php @@ -0,0 +1,714 @@ +run === true) { + throw new FireflyException('Cannot count after run in JournalCollector.'); + } + + $countQuery = clone $this->query; + + // dont need some fields: + $countQuery->getQuery()->limit = null; + $countQuery->getQuery()->offset = null; + $countQuery->getQuery()->unionLimit = null; + $countQuery->getQuery()->groups = null; + $countQuery->getQuery()->orders = null; + $countQuery->groupBy('accounts.user_id'); + $this->count = $countQuery->count(); + + return $this->count; + } + + /** + * @return JournalCollectorInterface + */ + public function disableFilter(): JournalCollectorInterface + { + $this->filterTransfers = false; + + return $this; + } + + /** + * @return JournalCollectorInterface + */ + public function disableInternalFilter(): JournalCollectorInterface + { + $this->filterInternalTransfers = false; + + return $this; + } + + /** + * @return JournalCollectorInterface + */ + public function enableInternalFilter(): JournalCollectorInterface + { + $this->filterInternalTransfers = true; + + return $this; + } + + /** + * @return Collection + */ + public function getJournals(): Collection + { + $this->run = true; + /** @var Collection $set */ + $set = $this->query->get(array_values($this->fields)); + Log::debug(sprintf('Count of set is %d', $set->count())); + $set = $this->filterTransfers($set); + Log::debug(sprintf('Count of set after filterTransfers() is %d', $set->count())); + + // possibly filter "internal" transfers: + $set = $this->filterInternalTransfers($set); + Log::debug(sprintf('Count of set after filterInternalTransfers() is %d', $set->count())); + + + // loop for decryption. + $set->each( + function (Transaction $transaction) { + $transaction->date = new Carbon($transaction->date); + $transaction->description = $transaction->encrypted ? Crypt::decrypt($transaction->description) : $transaction->description; + + if (!is_null($transaction->bill_name)) { + $transaction->bill_name = $transaction->bill_name_encrypted ? Crypt::decrypt($transaction->bill_name) : $transaction->bill_name; + } + + try { + $transaction->opposing_account_name = Crypt::decrypt($transaction->opposing_account_name); + } catch (DecryptException $e) { + // if this fails its already decrypted. + } + + } + ); + + return $set; + } + + /** + * @return LengthAwarePaginator + * @throws FireflyException + */ + public function getPaginatedJournals(): LengthAwarePaginator + { + if ($this->run === true) { + throw new FireflyException('Cannot getPaginatedJournals after run in JournalCollector.'); + } + $this->count(); + $set = $this->getJournals(); + $journals = new LengthAwarePaginator($set, $this->count, $this->limit, $this->page); + + return $journals; + } + + /** + * @param Collection $accounts + * + * @return JournalCollectorInterface + */ + public function setAccounts(Collection $accounts): JournalCollectorInterface + { + if ($accounts->count() > 0) { + $accountIds = $accounts->pluck('id')->toArray(); + $this->query->whereIn('transactions.account_id', $accountIds); + Log::debug(sprintf('setAccounts: %s', join(', ', $accountIds))); + $this->accountIds = $accountIds; + } + + if ($accounts->count() > 1) { + $this->filterTransfers = true; + } + + + return $this; + } + + /** + * @return JournalCollectorInterface + */ + public function setAllAssetAccounts(): JournalCollectorInterface + { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); + $accounts = $repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]); + if ($accounts->count() > 0) { + $accountIds = $accounts->pluck('id')->toArray(); + $this->query->whereIn('transactions.account_id', $accountIds); + $this->accountIds = $accountIds; + } + + if ($accounts->count() > 1) { + $this->filterTransfers = true; + } + + return $this; + } + + /** + * @param Collection $bills + * + * @return JournalCollectorInterface + */ + public function setBills(Collection $bills): JournalCollectorInterface + { + if ($bills->count() > 0) { + $billIds = $bills->pluck('id')->toArray(); + $this->query->whereIn('transaction_journals.bill_id', $billIds); + } + + return $this; + + } + + /** + * @param Budget $budget + * + * @return JournalCollectorInterface + */ + public function setBudget(Budget $budget): JournalCollectorInterface + { + $this->joinBudgetTables(); + + $this->query->where( + function (EloquentBuilder $q) use ($budget) { + $q->where('budget_transaction.budget_id', $budget->id); + $q->orWhere('budget_transaction_journal.budget_id', $budget->id); + } + ); + + return $this; + } + + /** + * @param Collection $budgets + * + * @return JournalCollectorInterface + */ + public function setBudgets(Collection $budgets): JournalCollectorInterface + { + $budgetIds = $budgets->pluck('id')->toArray(); + if (count($budgetIds) === 0) { + return $this; + } + $this->joinBudgetTables(); + + $this->query->where( + function (EloquentBuilder $q) use ($budgetIds) { + $q->whereIn('budget_transaction.budget_id', $budgetIds); + $q->orWhereIn('budget_transaction_journal.budget_id', $budgetIds); + } + ); + + return $this; + } + + /** + * @param Collection $categories + * + * @return JournalCollectorInterface + */ + public function setCategories(Collection $categories): JournalCollectorInterface + { + $categoryIds = $categories->pluck('id')->toArray(); + if (count($categoryIds) === 0) { + return $this; + } + $this->joinCategoryTables(); + + $this->query->where( + function (EloquentBuilder $q) use ($categoryIds) { + $q->whereIn('category_transaction.category_id', $categoryIds); + $q->orWhereIn('category_transaction_journal.category_id', $categoryIds); + } + ); + + return $this; + } + + /** + * @param Category $category + * + * @return JournalCollectorInterface + */ + public function setCategory(Category $category): JournalCollectorInterface + { + $this->joinCategoryTables(); + + $this->query->where( + function (EloquentBuilder $q) use ($category) { + $q->where('category_transaction.category_id', $category->id); + $q->orWhere('category_transaction_journal.category_id', $category->id); + } + ); + + return $this; + } + + /** + * @param int $limit + * + * @return JournalCollectorInterface + */ + public function setLimit(int $limit): JournalCollectorInterface + { + $this->limit = $limit; + $this->query->limit($limit); + Log::debug(sprintf('Set limit to %d', $limit)); + + return $this; + } + + /** + * @param int $offset + * + * @return JournalCollectorInterface + */ + public function setOffset(int $offset): JournalCollectorInterface + { + $this->offset = $offset; + + return $this; + } + + /** + * @param int $page + * + * @return JournalCollectorInterface + */ + public function setPage(int $page): JournalCollectorInterface + { + $this->page = $page; + + if ($page > 0) { + $page--; + } + Log::debug(sprintf('Page is %d', $page)); + + if (!is_null($this->limit)) { + $offset = ($this->limit * $page); + $this->offset = $offset; + $this->query->skip($offset); + Log::debug(sprintf('Changed offset to %d', $offset)); + } + if (is_null($this->limit)) { + Log::debug('The limit is zero, cannot set the page.'); + } + + return $this; + } + + /** + * @param Carbon $start + * @param Carbon $end + * + * @return JournalCollectorInterface + */ + public function setRange(Carbon $start, Carbon $end): JournalCollectorInterface + { + if ($start <= $end) { + $this->query->where('transaction_journals.date', '>=', $start->format('Y-m-d')); + $this->query->where('transaction_journals.date', '<=', $end->format('Y-m-d')); + } + + return $this; + } + + /** + * @param Tag $tag + * + * @return JournalCollectorInterface + */ + public function setTag(Tag $tag): JournalCollectorInterface + { + $this->joinTagTables(); + $this->query->where('tag_transaction_journal.tag_id', $tag->id); + + return $this; + } + + /** + * @param array $types + * + * @return JournalCollectorInterface + */ + public function setTypes(array $types): JournalCollectorInterface + { + if (count($types) > 0) { + Log::debug('Set query collector types', $types); + $this->query->whereIn('transaction_types.type', $types); + } + + return $this; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + + /** + * + */ + public function 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') + ->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'); + + $this->query = $query; + + } + + /** + * @return JournalCollectorInterface + */ + public function withBudgetInformation(): JournalCollectorInterface + { + $this->joinBudgetTables(); + + return $this; + } + + /** + * @return JournalCollectorInterface + */ + public function withCategoryInformation(): JournalCollectorInterface + { + + $this->joinCategoryTables(); + + return $this; + } + + /** + * @return JournalCollectorInterface + */ + public function withOpposingAccount(): JournalCollectorInterface + { + $this->joinOpposingTables(); + return $this; + } + + /** + * @return JournalCollectorInterface + */ + public function withoutBudget(): JournalCollectorInterface + { + $this->joinBudgetTables(); + + $this->query->where( + function (EloquentBuilder $q) { + $q->whereNull('budget_transaction.budget_id'); + $q->whereNull('budget_transaction_journal.budget_id'); + } + ); + + return $this; + } + + /** + * @return JournalCollectorInterface + */ + public function withoutCategory(): JournalCollectorInterface + { + $this->joinCategoryTables(); + + $this->query->where( + function (EloquentBuilder $q) { + $q->whereNull('category_transaction.category_id'); + $q->whereNull('category_transaction_journal.category_id'); + } + ); + + return $this; + } + + /** + * @param Collection $set + * + * @return Collection + */ + private function filterInternalTransfers(Collection $set): Collection + { + if ($this->filterInternalTransfers === false) { + Log::debug('Did NO filtering for internal transfers on given set.'); + + return $set; + } + if ($this->joinedOpposing === false) { + Log::info('Cannot filter internal transfers because no opposing information is present.'); + + return $set; + } + + $accountIds = $this->accountIds; + $set = $set->filter( + function (Transaction $transaction) use ($accountIds) { + // both id's in $accountids? + if (in_array($transaction->account_id, $accountIds) && in_array($transaction->opposing_account_id, $accountIds)) { + Log::debug( + sprintf( + 'Transaction #%d has #%d and #%d in set, so removed', + $transaction->id, $transaction->account_id, $transaction->opposing_account_id + ), $accountIds + ); + + return false; + } + + return $transaction; + + } + ); + + return $set; + } + + /** + * If the set of accounts used by the collector includes more than one asset + * account, chances are the set include double entries: transfers get selected + * on both the source, and then again on the destination account. + * + * This method filters them out by removing transfers that have been selected twice. + * + * @param Collection $set + * + * @return Collection + */ + private function filterTransfers(Collection $set): Collection + { + if ($this->filterTransfers) { + $count = []; + $new = new Collection; + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + if ($transaction->transaction_type_type !== TransactionType::TRANSFER) { + $new->push($transaction); + continue; + } + // make property string: + $journalId = $transaction->transaction_journal_id; + $amount = Steam::positive($transaction->transaction_amount); + $accountIds = [intval($transaction->account_id), intval($transaction->opposing_account_id)]; + sort($accountIds); + $key = $journalId . '-' . join(',', $accountIds) . '-' . $amount; + Log::debug(sprintf('Key is %s', $key)); + if (!isset($count[$key])) { + // not yet counted? add to new set and count it: + $new->push($transaction); + $count[$key] = 1; + } + } + + return $new; + } + + return $set; + } + + /** + * + */ + private function joinBudgetTables() + { + if (!$this->joinedBudget) { + // join some extra tables: + $this->joinedBudget = true; + $this->query->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); + $this->query->leftJoin('budgets as transaction_journal_budgets', 'transaction_journal_budgets.id', '=', 'budget_transaction_journal.budget_id'); + $this->query->leftJoin('budget_transaction', 'budget_transaction.transaction_id', '=', 'transactions.id'); + $this->query->leftJoin('budgets as transaction_budgets', 'transaction_budgets.id', '=', 'budget_transaction.budget_id'); + + $this->fields[] = 'budget_transaction_journal.budget_id as transaction_journal_budget_id'; + $this->fields[] = 'transaction_journal_budgets.encrypted as transaction_journal_budget_encrypted'; + $this->fields[] = 'transaction_journal_budgets.name as transaction_journal_budget_name'; + + $this->fields[] = 'budget_transaction.budget_id as transaction_budget_id'; + $this->fields[] = 'transaction_budgets.encrypted as transaction_budget_encrypted'; + $this->fields[] = 'transaction_budgets.name as transaction_budget_name'; + } + } + + /** + * + */ + private function joinCategoryTables() + { + if (!$this->joinedCategory) { + // join some extra tables: + $this->joinedCategory = true; + $this->query->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); + $this->query->leftJoin( + 'categories as transaction_journal_categories', 'transaction_journal_categories.id', '=', 'category_transaction_journal.category_id' + ); + + $this->query->leftJoin('category_transaction', 'category_transaction.transaction_id', '=', 'transactions.id'); + $this->query->leftJoin('categories as transaction_categories', 'transaction_categories.id', '=', 'category_transaction.category_id'); + + $this->fields[] = 'category_transaction_journal.category_id as transaction_journal_category_id'; + $this->fields[] = 'transaction_journal_categories.encrypted as transaction_journal_category_encrypted'; + $this->fields[] = 'transaction_journal_categories.name as transaction_journal_category_name'; + + $this->fields[] = 'category_transaction.category_id as transaction_category_id'; + $this->fields[] = 'transaction_categories.encrypted as transaction_category_encrypted'; + $this->fields[] = 'transaction_categories.name as transaction_category_name'; + } + } + + /** + * + */ + private function joinOpposingTables() + { + if (!$this->joinedOpposing) { + Log::debug('joinedOpposing is false'); + // join opposing transaction (hard): + $this->query->leftJoin( + 'transactions as opposing', function (JoinClause $join) { + $join->on('opposing.transaction_journal_id', '=', 'transactions.transaction_journal_id') + ->where('opposing.identifier', '=', DB::raw('transactions.identifier')) + ->where('opposing.amount', '=', DB::raw('transactions.amount * -1')); + } + ); + $this->query->leftJoin('accounts as opposing_accounts', 'opposing.account_id', '=', 'opposing_accounts.id'); + $this->query->leftJoin('account_types as opposing_account_types', 'opposing_accounts.account_type_id', '=', 'opposing_account_types.id'); + $this->query->whereNull('opposing.deleted_at'); + + $this->fields[] = 'opposing.account_id as opposing_account_id'; + $this->fields[] = 'opposing_accounts.name as opposing_account_name'; + $this->fields[] = 'opposing_accounts.encrypted as opposing_account_encrypted'; + $this->fields[] = 'opposing_account_types.type as opposing_account_type'; + $this->joinedOpposing = true; + Log::debug('joinedOpposing is now true!'); + } + } + + /** + * + */ + private function joinTagTables() + { + if (!$this->joinedTag) { + // join some extra tables: + $this->joinedTag = true; + $this->query->leftJoin('tag_transaction_journal', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); + } + } +} diff --git a/app/Helpers/Collector/JournalCollectorInterface.php b/app/Helpers/Collector/JournalCollectorInterface.php new file mode 100644 index 0000000000..80e41eb781 --- /dev/null +++ b/app/Helpers/Collector/JournalCollectorInterface.php @@ -0,0 +1,182 @@ +' . strval(trans('firefly.route_has_no_help')) . '

'; - $result = Requests::get($uri); + $uri = sprintf('https://raw.githubusercontent.com/firefly-iii/help/master/%s/%s.md', $language, $route); + Log::debug(sprintf('Trying to get %s...', $uri)); + $opt = ['useragent' => $this->userAgent]; + $content = ''; + try { + $result = Requests::get($uri, [], $opt); + } catch (Requests_Exception $e) { + Log::error($e); + + return ''; + } + + + Log::debug(sprintf('Status code is %d', $result->status_code)); if ($result->status_code === 200) { - $content = $result->body; + $content = trim($result->body); } - - - if (strlen(trim($content)) == 0) { - $content = '

' . strval(trans('firefly.route_has_no_help')) . '

'; + if (strlen($content) > 0) { + Log::debug('Content is longer than zero. Expect something.'); + $converter = new CommonMarkConverter(); + $content = $converter->convertToHtml($content); } - $converter = new CommonMarkConverter(); - $content = $converter->convertToHtml($content); return $content; - } /** @@ -70,7 +84,7 @@ class Help implements HelpInterface * * @return bool */ - public function hasRoute(string $route):bool + public function hasRoute(string $route): bool { return Route::has($route); } @@ -81,9 +95,19 @@ class Help implements HelpInterface * * @return bool */ - public function inCache(string $route, string $language):bool + public function inCache(string $route, string $language): bool { - return Cache::has('help.' . $route . '.' . $language); + $line = sprintf('help.%s.%s', $route, $language); + $result = Cache::has($line); + if ($result) { + Log::debug(sprintf('Cache has this entry: %s', 'help.' . $route . '.' . $language)); + } + if (!$result) { + Log::debug(sprintf('Cache does not have this entry: %s', 'help.' . $route . '.' . $language)); + } + + return $result; + } /** @@ -92,10 +116,15 @@ class Help implements HelpInterface * @param string $language * @param string $content * - * @internal param $title */ public function putInCache(string $route, string $language, string $content) { - Cache::put('help.' . $route . '.' . $language, $content, 10080); // a week. + $key = sprintf('help.%s.%s', $route, $language); + if (strlen($content) > 0) { + Log::debug(sprintf('Will store entry in cache: %s', $key)); + Cache::put($key, $content, 10080); // a week. + return; + } + Log::info(sprintf('Will not cache %s because content is empty.', $key)); } } diff --git a/app/Helpers/Help/HelpInterface.php b/app/Helpers/Help/HelpInterface.php index 7356ba0ba2..d642f68294 100644 --- a/app/Helpers/Help/HelpInterface.php +++ b/app/Helpers/Help/HelpInterface.php @@ -34,7 +34,7 @@ interface HelpInterface * * @return string */ - public function getFromGithub(string $language, string $route):string; + public function getFromGithub(string $language, string $route): string; /** * @param string $route @@ -49,7 +49,7 @@ interface HelpInterface * * @return bool */ - public function inCache(string $route, string $language ): bool; + public function inCache(string $route, string $language): bool; /** * @param string $route diff --git a/app/Helpers/Report/BalanceReportHelper.php b/app/Helpers/Report/BalanceReportHelper.php index 4210cc66fe..ca0667cf66 100644 --- a/app/Helpers/Report/BalanceReportHelper.php +++ b/app/Helpers/Report/BalanceReportHelper.php @@ -19,17 +19,18 @@ use FireflyIII\Helpers\Collection\Balance; use FireflyIII\Helpers\Collection\BalanceEntry; use FireflyIII\Helpers\Collection\BalanceHeader; use FireflyIII\Helpers\Collection\BalanceLine; -use FireflyIII\Models\Budget; -use FireflyIII\Models\LimitRepetition; +use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; +use Log; /** * Class BalanceReportHelper * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) // I can't really help it. * @package FireflyIII\Helpers\Report */ class BalanceReportHelper implements BalanceReportHelperInterface @@ -51,27 +52,29 @@ class BalanceReportHelper implements BalanceReportHelperInterface /** + * @param Collection $accounts * @param Carbon $start * @param Carbon $end - * @param Collection $accounts * * @return Balance */ - public function getBalanceReport(Carbon $start, Carbon $end, Collection $accounts): Balance + public function getBalanceReport(Collection $accounts, Carbon $start, Carbon $end): Balance { - $balance = new Balance; - $header = new BalanceHeader; - $limitRepetitions = $this->budgetRepository->getAllBudgetLimitRepetitions($start, $end); + Log::debug('Start of balance report'); + $balance = new Balance; + $header = new BalanceHeader; + $budgetLimits = $this->budgetRepository->getAllBudgetLimits($start, $end); foreach ($accounts as $account) { + Log::debug(sprintf('Add account %s to headers.', $account->name)); $header->addAccount($account); } - /** @var LimitRepetition $repetition */ - foreach ($limitRepetitions as $repetition) { - $budget = $this->budgetRepository->find($repetition->budget_id); - $line = $this->createBalanceLine($budget, $repetition, $accounts); + /** @var BudgetLimit $budgetLimit */ + foreach ($budgetLimits as $budgetLimit) { + $line = $this->createBalanceLine($budgetLimit, $accounts); $balance->addBalanceLine($line); } + Log::debug('Create rest of the things.'); $noBudgetLine = $this->createNoBudgetLine($accounts, $start, $end); $coveredByTagLine = $this->createTagsBalanceLine($accounts, $start, $end); $leftUnbalancedLine = $this->createLeftUnbalancedLine($noBudgetLine, $coveredByTagLine); @@ -81,9 +84,12 @@ class BalanceReportHelper implements BalanceReportHelperInterface $balance->addBalanceLine($leftUnbalancedLine); $balance->setBalanceHeader($header); + Log::debug('Clear unused budgets.'); // remove budgets without expenses from balance lines: $balance = $this->removeUnusedBudgets($balance); + Log::debug('Return report.'); + return $balance; } @@ -137,27 +143,22 @@ class BalanceReportHelper implements BalanceReportHelperInterface /** - * @param Budget $budget - * @param LimitRepetition $repetition - * @param Collection $accounts + * @param BudgetLimit $budgetLimit + * @param Collection $accounts * * @return BalanceLine */ - private function createBalanceLine(Budget $budget, LimitRepetition $repetition, Collection $accounts): BalanceLine + private function createBalanceLine(BudgetLimit $budgetLimit, Collection $accounts): BalanceLine { - $line = new BalanceLine; - $budget->amount = $repetition->amount; - $line->setBudget($budget); - $line->setStartDate($repetition->startdate); - $line->setEndDate($repetition->enddate); + $line = new BalanceLine; + $line->setBudget($budgetLimit->budget); + $line->setBudgetLimit($budgetLimit); // loop accounts: foreach ($accounts as $account) { $balanceEntry = new BalanceEntry; $balanceEntry->setAccount($account); - $spent = $this->budgetRepository->spentInPeriod( - new Collection([$budget]), new Collection([$account]), $repetition->startdate, $repetition->enddate - ); + $spent = $this->budgetRepository->spentInPeriod(new Collection([$budgetLimit->budget]), new Collection([$account]), $budgetLimit->start_date, $budgetLimit->end_date); $balanceEntry->setSpent($spent); $line->addBalanceEntry($balanceEntry); } @@ -212,7 +213,7 @@ class BalanceReportHelper implements BalanceReportHelperInterface $empty = new BalanceLine; foreach ($accounts as $account) { - $spent = $this->budgetRepository->spentInPeriodWithoutBudget(new Collection([$account]), $start, $end); + $spent = $this->budgetRepository->spentInPeriodWoBudget(new Collection([$account]), $start, $end); // budget $budgetEntry = new BalanceEntry; $budgetEntry->setAccount($account); diff --git a/app/Helpers/Report/BalanceReportHelperInterface.php b/app/Helpers/Report/BalanceReportHelperInterface.php index 5aa14d09cb..6687eb6cf6 100644 --- a/app/Helpers/Report/BalanceReportHelperInterface.php +++ b/app/Helpers/Report/BalanceReportHelperInterface.php @@ -26,11 +26,11 @@ use Illuminate\Support\Collection; interface BalanceReportHelperInterface { /** + * @param Collection $accounts * @param Carbon $start * @param Carbon $end - * @param Collection $accounts * * @return Balance */ - public function getBalanceReport(Carbon $start, Carbon $end, Collection $accounts): Balance; + public function getBalanceReport(Collection $accounts, Carbon $start, Carbon $end): Balance; } diff --git a/app/Helpers/Report/BudgetReportHelper.php b/app/Helpers/Report/BudgetReportHelper.php index 9c250e8560..8025b0a159 100644 --- a/app/Helpers/Report/BudgetReportHelper.php +++ b/app/Helpers/Report/BudgetReportHelper.php @@ -15,12 +15,9 @@ namespace FireflyIII\Helpers\Report; use Carbon\Carbon; -use FireflyIII\Helpers\Collection\Budget as BudgetCollection; -use FireflyIII\Helpers\Collection\BudgetLine; use FireflyIII\Models\Budget; -use FireflyIII\Models\LimitRepetition; +use FireflyIII\Models\BudgetLimit; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; -use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; /** @@ -44,112 +41,68 @@ class BudgetReportHelper implements BudgetReportHelperInterface } /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) // at 43, its ok. * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly 5. - * * @param Carbon $start * @param Carbon $end * @param Collection $accounts * - * @return Collection + * @return array */ - public function budgetYearOverview(Carbon $start, Carbon $end, Collection $accounts): Collection + public function getBudgetReport(Carbon $start, Carbon $end, Collection $accounts): array { - // chart properties for cache: - $cache = new CacheProperties; - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('budget-year'); - $cache->addProperty($accounts->pluck('id')->toArray()); - if ($cache->has()) { - return $cache->get(); - } - - $current = clone $start; - $return = new Collection; - $set = $this->repository->getBudgets(); - $budgets = []; - $spent = []; - $headers = $this->createYearHeaders($current, $end); + $set = $this->repository->getBudgets(); + $array = []; /** @var Budget $budget */ foreach ($set as $budget) { - $id = $budget->id; - $budgets[$id] = $budget->name; - $current = clone $start; - $budgetData = $this->getBudgetSpentData($current, $end, $budget, $accounts); - $sum = $budgetData['sum']; - $spent[$id] = $budgetData['spent']; + $budgetLimits = $this->repository->getBudgetLimits($budget, $start, $end); + if ($budgetLimits->count() == 0) { // no budget limit(s) for this budget - if (bccomp('0', $sum) === 0) { - // not spent anything. - unset($spent[$id]); - unset($budgets[$id]); - } - } - - $return->put('headers', $headers); - $return->put('budgets', $budgets); - $return->put('spent', $spent); - - $cache->store($return); - - return $return; - } - - /** - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return BudgetCollection - */ - public function getBudgetReport(Carbon $start, Carbon $end, Collection $accounts): BudgetCollection - { - $object = new BudgetCollection; - $set = $this->repository->getBudgets(); - - /** @var Budget $budget */ - foreach ($set as $budget) { - $repetitions = $budget->limitrepetitions()->before($end)->after($start)->get(); - - // no repetition(s) for this budget: - if ($repetitions->count() == 0) { - // spent for budget in time range: - $spent = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end); - - if ($spent > 0) { - $budgetLine = new BudgetLine; - $budgetLine->setBudget($budget)->setOverspent($spent); - $object->addOverspent($spent)->addBudgetLine($budgetLine); + $spent = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end);// spent for budget in time range + if (bccomp($spent, '0') === -1) { + $line = [ + 'type' => 'budget', + 'id' => $budget->id, + 'name' => $budget->name, + 'budgeted' => '0', + 'spent' => $spent, + 'left' => '0', + 'overspent' => '0', + ]; + $array[] = $line; } continue; } - // one or more repetitions for budget: - /** @var LimitRepetition $repetition */ - foreach ($repetitions as $repetition) { - $data = $this->calculateExpenses($budget, $repetition, $accounts); - - $budgetLine = new BudgetLine; - $budgetLine->setBudget($budget)->setRepetition($repetition) - ->setLeft($data['left'])->setSpent($data['expenses'])->setOverspent($data['overspent']) - ->setBudgeted(strval($repetition->amount)); - - $object->addBudgeted(strval($repetition->amount))->addSpent($data['spent']) - ->addLeft($data['left'])->addOverspent($data['overspent'])->addBudgetLine($budgetLine); + /** @var BudgetLimit $budgetLimit */ + foreach ($budgetLimits as $budgetLimit) { // one or more repetitions for budget + $data = $this->calculateExpenses($budget, $budgetLimit, $accounts); + $line = [ + 'type' => 'budget-line', + 'start' => $budgetLimit->start_date, + 'end' => $budgetLimit->end_date, + 'limit' => $budgetLimit->id, + 'id' => $budget->id, + 'name' => $budget->name, + 'budgeted' => strval($budgetLimit->amount), + 'spent' => $data['expenses'], + 'left' => $data['left'], + 'overspent' => $data['overspent'], + ]; + $array[] = $line; } - } + $noBudget = $this->repository->spentInPeriodWoBudget($accounts, $start, $end); // stuff outside of budgets + $line = [ + 'type' => 'no-budget', + 'budgeted' => '0', + 'spent' => $noBudget, + 'left' => '0', + 'overspent' => '0', + ]; + $array[] = $line; - // stuff outside of budgets: - - $noBudget = $this->repository->spentInPeriodWithoutBudget($accounts, $start, $end); - $budgetLine = new BudgetLine; - $budgetLine->setOverspent($noBudget)->setSpent($noBudget); - $object->addOverspent($noBudget)->addBudgetLine($budgetLine); - - return $object; + return $array; } /** @@ -183,95 +136,22 @@ class BudgetReportHelper implements BudgetReportHelperInterface } /** - * Take the array as returned by CategoryRepositoryInterface::spentPerDay and CategoryRepositoryInterface::earnedByDay - * and sum up everything in the array in the given range. - * - * @param Carbon $start - * @param Carbon $end - * @param array $array - * - * @return string - */ - protected function getSumOfRange(Carbon $start, Carbon $end, array $array) - { - $sum = '0'; - $currentStart = clone $start; // to not mess with the original one - $currentEnd = clone $end; // to not mess with the original one - - while ($currentStart <= $currentEnd) { - $date = $currentStart->format('Y-m-d'); - if (isset($array[$date])) { - $sum = bcadd($sum, $array[$date]); - } - $currentStart->addDay(); - } - - return $sum; - } - - /** - * @param Budget $budget - * @param LimitRepetition $repetition - * @param Collection $accounts + * @param Budget $budget + * @param BudgetLimit $budgetLimit + * @param Collection $accounts * * @return array */ - private function calculateExpenses(Budget $budget, LimitRepetition $repetition, Collection $accounts): array + private function calculateExpenses(Budget $budget, BudgetLimit $budgetLimit, Collection $accounts): array { $array = []; - $expenses = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $repetition->startdate, $repetition->enddate); - $array['left'] = bccomp(bcadd($repetition->amount, $expenses), '0') === 1 ? bcadd($repetition->amount, $expenses) : '0'; - $array['spent'] = bccomp(bcadd($repetition->amount, $expenses), '0') === 1 ? $expenses : '0'; - $array['overspent'] = bccomp(bcadd($repetition->amount, $expenses), '0') === 1 ? '0' : bcadd($expenses, $repetition->amount); + $expenses = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $budgetLimit->start_date, $budgetLimit->end_date); + $array['left'] = bccomp(bcadd($budgetLimit->amount, $expenses), '0') === 1 ? bcadd($budgetLimit->amount, $expenses) : '0'; + $array['spent'] = bccomp(bcadd($budgetLimit->amount, $expenses), '0') === 1 ? $expenses : '0'; + $array['overspent'] = bccomp(bcadd($budgetLimit->amount, $expenses), '0') === 1 ? '0' : bcadd($expenses, $budgetLimit->amount); $array['expenses'] = $expenses; return $array; } - - /** - * @param Carbon $current - * @param Carbon $end - * - * @return array - */ - private function createYearHeaders(Carbon $current, Carbon $end): array - { - $headers = []; - while ($current < $end) { - $short = $current->format('m-Y'); - $headers[$short] = $current->formatLocalized((string)trans('config.month')); - $current->addMonth(); - } - - return $headers; - } - - /** - * @param Carbon $current - * @param Carbon $end - * @param Budget $budget - * @param Collection $accounts - * - * @return array - */ - private function getBudgetSpentData(Carbon $current, Carbon $end, Budget $budget, Collection $accounts): array - { - $sum = '0'; - $spent = []; - while ($current < $end) { - $currentEnd = clone $current; - $currentEnd->endOfMonth(); - $format = $current->format('m-Y'); - $budgetSpent = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $current, $currentEnd); - $spent[$format] = $budgetSpent; - $sum = bcadd($sum, $budgetSpent); - $current->addMonth(); - } - - return [ - 'spent' => $spent, - 'sum' => $sum, - ]; - } } diff --git a/app/Helpers/Report/BudgetReportHelperInterface.php b/app/Helpers/Report/BudgetReportHelperInterface.php index 3660447c99..a64d47d39f 100644 --- a/app/Helpers/Report/BudgetReportHelperInterface.php +++ b/app/Helpers/Report/BudgetReportHelperInterface.php @@ -15,7 +15,6 @@ namespace FireflyIII\Helpers\Report; use Carbon\Carbon; -use FireflyIII\Helpers\Collection\Budget as BudgetCollection; use Illuminate\Support\Collection; /** @@ -25,23 +24,15 @@ use Illuminate\Support\Collection; */ interface BudgetReportHelperInterface { - /** - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return Collection - */ - public function budgetYearOverview(Carbon $start, Carbon $end, Collection $accounts): Collection; /** * @param Carbon $start * @param Carbon $end * @param Collection $accounts * - * @return BudgetCollection + * @return array */ - public function getBudgetReport(Carbon $start, Carbon $end, Collection $accounts): BudgetCollection; + public function getBudgetReport(Carbon $start, Carbon $end, Collection $accounts): array; /** * @param Carbon $start diff --git a/app/Helpers/Report/ReportHelper.php b/app/Helpers/Report/ReportHelper.php index f9df70c397..2dfb605ee3 100644 --- a/app/Helpers/Report/ReportHelper.php +++ b/app/Helpers/Report/ReportHelper.php @@ -17,22 +17,15 @@ use Carbon\Carbon; use FireflyIII\Helpers\Collection\Bill as BillCollection; use FireflyIII\Helpers\Collection\BillLine; use FireflyIII\Helpers\Collection\Category as CategoryCollection; -use FireflyIII\Helpers\Collection\Expense; -use FireflyIII\Helpers\Collection\Income; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Helpers\FiscalHelperInterface; use FireflyIII\Models\Bill; use FireflyIII\Models\Category; -use FireflyIII\Models\Tag; -use FireflyIII\Models\TransactionJournal; -use FireflyIII\Repositories\Account\AccountTaskerInterface; +use FireflyIII\Models\Transaction; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; -use FireflyIII\Repositories\Tag\TagRepositoryInterface; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; -use stdClass; /** * Class ReportHelper @@ -44,26 +37,24 @@ class ReportHelper implements ReportHelperInterface /** @var BudgetRepositoryInterface */ protected $budgetRepository; - /** @var TagRepositoryInterface */ - protected $tagRepository; /** * ReportHelper constructor. * * * @param BudgetRepositoryInterface $budgetRepository - * @param TagRepositoryInterface $tagRepository */ - public function __construct(BudgetRepositoryInterface $budgetRepository, TagRepositoryInterface $tagRepository) + public function __construct(BudgetRepositoryInterface $budgetRepository) { $this->budgetRepository = $budgetRepository; - $this->tagRepository = $tagRepository; } /** * This method generates a full report for the given period on all * the users bills and their payments. * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly 5. + * * Excludes bills which have not had a payment on the mentioned accounts. * * @param Carbon $start @@ -77,116 +68,41 @@ class ReportHelper implements ReportHelperInterface /** @var BillRepositoryInterface $repository */ $repository = app(BillRepositoryInterface::class); $bills = $repository->getBillsForAccounts($accounts); - $journals = $repository->getAllJournalsInRange($bills, $start, $end); + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setBills($bills); + $journals = $collector->getJournals(); $collection = new BillCollection; + $collection->setStartDate($start); + $collection->setEndDate($end); /** @var Bill $bill */ foreach ($bills as $bill) { $billLine = new BillLine; $billLine->setBill($bill); - $billLine->setActive(intval($bill->active) === 1); $billLine->setMin(strval($bill->amount_min)); $billLine->setMax(strval($bill->amount_max)); $billLine->setHit(false); - // is hit in period? - $entry = $journals->filter( - function (TransactionJournal $journal) use ($bill) { - return $journal->bill_id === $bill->id; + function (Transaction $transaction) use ($bill) { + return $transaction->bill_id === $bill->id; } ); $first = $entry->first(); if (!is_null($first)) { $billLine->setTransactionJournalId($first->id); - $billLine->setAmount($first->journalAmount); + $billLine->setAmount($first->transaction_amount); + $billLine->setLastHitDate($first->date); $billLine->setHit(true); } - - // non active AND non hit? do not add: if ($billLine->isActive() || $billLine->isHit()) { $collection->addBill($billLine); } } + $collection->filterBills(); return $collection; } - /** - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return CategoryCollection - */ - public function getCategoryReport(Carbon $start, Carbon $end, Collection $accounts): CategoryCollection - { - $object = new CategoryCollection; - /** @var CategoryRepositoryInterface $repository */ - $repository = app(CategoryRepositoryInterface::class); - $categories = $repository->getCategories(); - - /** @var Category $category */ - foreach ($categories as $category) { - $spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $start, $end); - // CategoryCollection expects the amount in $spent: - $category->spent = $spent; - $object->addCategory($category); - } - - return $object; - } - - /** - * Get a full report on the users expenses during the period for a list of accounts. - * - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return Expense - */ - public function getExpenseReport(Carbon $start, Carbon $end, Collection $accounts): Expense - { - $object = new Expense; - - /** @var AccountTaskerInterface $tasker */ - $tasker = app(AccountTaskerInterface::class); - $collection = $tasker->expenseReport($accounts, $accounts, $start, $end); - - /** @var stdClass $entry */ - foreach ($collection as $entry) { - $object->addToTotal($entry->amount); - $object->addOrCreateExpense($entry); - } - - return $object; - } - - /** - * Get a full report on the users incomes during the period for the given accounts. - * - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return Income - */ - public function getIncomeReport(Carbon $start, Carbon $end, Collection $accounts): Income - { - $object = new Income; - /** @var AccountTaskerInterface $tasker */ - $tasker = app(AccountTaskerInterface::class); - $collection = $tasker->incomeReport($accounts, $accounts, $start, $end); - - /** @var stdClass $entry */ - foreach ($collection as $entry) { - $object->addToTotal($entry->amount); - $object->addOrCreateIncome($entry); - } - - return $object; - } - /** * @param Carbon $date * @@ -231,104 +147,4 @@ class ReportHelper implements ReportHelperInterface return $months; } - /** - * Returns an array of tags and their comparitive size with amounts bla bla. - * - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return array - */ - public function tagReport(Carbon $start, Carbon $end, Collection $accounts): array - { - $ids = $accounts->pluck('id')->toArray(); - $set = Tag:: - leftJoin('tag_transaction_journal', 'tags.id', '=', 'tag_transaction_journal.tag_id') - ->leftJoin('transaction_journals', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin( - 'transactions AS source', function (JoinClause $join) { - $join->on('source.transaction_journal_id', '=', 'transaction_journals.id')->where('source.amount', '<', '0'); - } - ) - ->leftJoin( - 'transactions AS destination', function (JoinClause $join) { - $join->on('destination.transaction_journal_id', '=', 'transaction_journals.id')->where('destination.amount', '>', '0'); - } - ) - ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) - ->where( - // source.account_id in accountIds XOR destination.account_id in accountIds - function (Builder $query) use ($ids) { - $query->where( - function (Builder $q1) use ($ids) { - $q1->whereIn('source.account_id', $ids) - ->whereNotIn('destination.account_id', $ids); - } - )->orWhere( - function (Builder $q2) use ($ids) { - $q2->whereIn('destination.account_id', $ids) - ->whereNotIn('source.account_id', $ids); - } - ); - } - ) - ->get(['tags.id', 'tags.tag', 'transaction_journals.id as journal_id', 'destination.amount']); - $collection = []; - if ($set->count() === 0) { - return $collection; - } - /** @var Tag $entry */ - foreach ($set as $entry) { - // less than zero? multiply to be above zero. - $amount = $entry->amount; - $id = intval($entry->id); - $previousAmount = $collection[$id]['amount'] ?? '0'; - $collection[$id] = [ - 'id' => $id, - 'tag' => $entry->tag, - 'amount' => bcadd($previousAmount, $amount), - ]; - } - - // cleanup collection (match "fonts") - $max = strval(max(array_column($collection, 'amount'))); - foreach ($collection as $id => $entry) { - $size = bcdiv($entry['amount'], $max, 4); - if (bccomp($size, '0.25') === -1) { - $size = '0.5'; - } - $collection[$id]['fontsize'] = $size; - } - - return $collection; - } - - /** - * Take the array as returned by CategoryRepositoryInterface::spentPerDay and CategoryRepositoryInterface::earnedByDay - * and sum up everything in the array in the given range. - * - * @param Carbon $start - * @param Carbon $end - * @param array $array - * - * @return string - */ - protected function getSumOfRange(Carbon $start, Carbon $end, array $array) - { - $sum = '0'; - $currentStart = clone $start; // to not mess with the original one - $currentEnd = clone $end; // to not mess with the original one - - while ($currentStart <= $currentEnd) { - $date = $currentStart->format('Y-m-d'); - if (isset($array[$date])) { - $sum = bcadd($sum, $array[$date]); - } - $currentStart->addDay(); - } - - return $sum; - } } diff --git a/app/Helpers/Report/ReportHelperInterface.php b/app/Helpers/Report/ReportHelperInterface.php index 34ffcc9e7b..9124821593 100644 --- a/app/Helpers/Report/ReportHelperInterface.php +++ b/app/Helpers/Report/ReportHelperInterface.php @@ -42,37 +42,6 @@ interface ReportHelperInterface */ public function getBillReport(Carbon $start, Carbon $end, Collection $accounts): BillCollection; - /** - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return CategoryCollection - */ - public function getCategoryReport(Carbon $start, Carbon $end, Collection $accounts): CategoryCollection; - - /** - * Get a full report on the users expenses during the period for a list of accounts. - * - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return Expense - */ - public function getExpenseReport(Carbon $start, Carbon $end, Collection $accounts): Expense; - - /** - * Get a full report on the users incomes during the period for the given accounts. - * - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return Income - */ - public function getIncomeReport(Carbon $start, Carbon $end, Collection $accounts): Income; - /** * @param Carbon $date * @@ -80,15 +49,4 @@ interface ReportHelperInterface */ public function listOfMonths(Carbon $date): array; - /** - * Returns an array of tags and their comparitive size with amounts bla bla. - * - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return array - */ - public function tagReport(Carbon $start, Carbon $end, Collection $accounts): array; - } diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index d11462dc02..66b2ff26a0 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -13,24 +13,26 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers; +use Amount; use Carbon\Carbon; use ExpandedForm; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Requests\AccountFormRequest; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction; -use FireflyIII\Repositories\Account\AccountRepositoryInterface as ARI; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountTaskerInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\CacheProperties; -use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Http\Request; use Illuminate\Support\Collection; -use Input; +use Log; use Navigation; use Preferences; use Session; use Steam; -use URL; use View; /** @@ -46,40 +48,59 @@ class AccountController extends Controller public function __construct() { parent::__construct(); - View::share('mainTitleIcon', 'fa-credit-card'); - View::share('title', trans('firefly.accounts')); + + // translations: + $this->middleware( + function ($request, $next) { + View::share('mainTitleIcon', 'fa-credit-card'); + View::share('title', trans('firefly.accounts')); + + return $next($request); + } + ); } /** * @param string $what * - * @return View + * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory|View */ public function create(string $what = 'asset') { - $subTitleIcon = config('firefly.subIconsByIdentifier.' . $what); - $subTitle = trans('firefly.make_new_' . $what . '_account'); - Session::flash('preFilled', []); + /** @var CurrencyRepositoryInterface $repository */ + $repository = app(CurrencyRepositoryInterface::class); + $currencies = ExpandedForm::makeSelectList($repository->get()); + $defaultCurrency = Amount::getDefaultCurrency(); + $subTitleIcon = config('firefly.subIconsByIdentifier.' . $what); + $subTitle = trans('firefly.make_new_' . $what . '_account'); + $roles = []; + foreach (config('firefly.accountRoles') as $role) { + $roles[$role] = strval(trans('firefly.account_role_' . $role)); + } + + + // pre fill some data + Session::flash('preFilled', ['currency_id' => $defaultCurrency->id,]); // put previous url in session if not redirect from store (not "create another"). if (session('accounts.create.fromStore') !== true) { - Session::put('accounts.create.url', URL::previous()); + $this->rememberPreviousUri('accounts.create.uri'); } Session::forget('accounts.create.fromStore'); Session::flash('gaEventCategory', 'accounts'); Session::flash('gaEventAction', 'create-' . $what); - return view('accounts.create', compact('subTitleIcon', 'what', 'subTitle')); + return view('accounts.create', compact('subTitleIcon', 'what', 'subTitle', 'currencies', 'roles')); } /** - * @param ARI $repository - * @param Account $account + * @param AccountRepositoryInterface $repository + * @param Account $account * * @return View */ - public function delete(ARI $repository, Account $account) + public function delete(AccountRepositoryInterface $repository, Account $account) { $typeName = config('firefly.shortNamesByFullName.' . $account->accountType->type); $subTitle = trans('firefly.delete_' . $typeName . '_account', ['name' => $account->name]); @@ -87,7 +108,7 @@ class AccountController extends Controller unset($accountList[$account->id]); // put previous url in session - Session::put('accounts.delete.url', URL::previous()); + $this->rememberPreviousUri('accounts.delete.uri'); Session::flash('gaEventCategory', 'accounts'); Session::flash('gaEventAction', 'delete-' . $typeName); @@ -95,24 +116,25 @@ class AccountController extends Controller } /** - * @param ARI $repository + * @param Request $request + * @param AccountRepositoryInterface $repository * @param Account $account * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function destroy(ARI $repository, Account $account) + public function destroy(Request $request, AccountRepositoryInterface $repository, Account $account) { $type = $account->accountType->type; $typeName = config('firefly.shortNamesByFullName.' . $type); $name = $account->name; - $moveTo = $repository->find(intval(Input::get('move_account_before_delete'))); + $moveTo = $repository->find(intval($request->get('move_account_before_delete'))); $repository->destroy($account, $moveTo); Session::flash('success', strval(trans('firefly.' . $typeName . '_deleted', ['name' => $name]))); Preferences::mark(); - return redirect(session('accounts.delete.url')); + return redirect($this->getPreviousUri('accounts.delete.uri')); } /** @@ -126,10 +148,18 @@ class AccountController extends Controller $what = config('firefly.shortNamesByFullName')[$account->accountType->type]; $subTitle = trans('firefly.edit_' . $what . '_account', ['name' => $account->name]); $subTitleIcon = config('firefly.subIconsByIdentifier.' . $what); + /** @var CurrencyRepositoryInterface $repository */ + $repository = app(CurrencyRepositoryInterface::class); + $currencies = ExpandedForm::makeSelectList($repository->get()); + $roles = []; + foreach (config('firefly.accountRoles') as $role) { + $roles[$role] = strval(trans('firefly.account_role_' . $role)); + } + // put previous url in session if not redirect from store (not "return_to_edit"). if (session('accounts.edit.fromUpdate') !== true) { - Session::put('accounts.edit.url', URL::previous()); + $this->rememberPreviousUri('accounts.edit.uri'); } Session::forget('accounts.edit.fromUpdate'); @@ -146,27 +176,28 @@ class AccountController extends Controller 'accountRole' => $account->getMeta('accountRole'), 'ccType' => $account->getMeta('ccType'), 'ccMonthlyPaymentDate' => $account->getMeta('ccMonthlyPaymentDate'), + 'BIC' => $account->getMeta('BIC'), 'openingBalanceDate' => $openingBalanceDate, 'openingBalance' => $openingBalanceAmount, 'virtualBalance' => $account->virtual_balance, + 'currency_id' => $account->getMeta('currency_id'), ]; Session::flash('preFilled', $preFilled); Session::flash('gaEventCategory', 'accounts'); Session::flash('gaEventAction', 'edit-' . $what); - return view('accounts.edit', compact('account', 'subTitle', 'subTitleIcon', 'openingBalance', 'what')); + return view('accounts.edit', compact('currencies', 'account', 'subTitle', 'subTitleIcon', 'openingBalance', 'what', 'roles')); } /** - * @param ARI $repository + * @param AccountRepositoryInterface $repository * @param string $what * * @return View */ - public function index(ARI $repository, string $what) + public function index(AccountRepositoryInterface $repository, string $what) { - $what = $what ?? 'asset'; - + $what = $what ?? 'asset'; $subTitle = trans('firefly.' . $what . '_accounts'); $subTitleIcon = config('firefly.subIconsByIdentifier.' . $what); $types = config('firefly.accountTypesByIdentifier.' . $what); @@ -187,6 +218,7 @@ class AccountController extends Controller $account->lastActivityDate = $this->isInArray($activities, $account->id); $account->startBalance = $this->isInArray($startBalances, $account->id); $account->endBalance = $this->isInArray($endBalances, $account->id); + $account->difference = bcsub($account->endBalance, $account->startBalance); } ); @@ -194,13 +226,13 @@ class AccountController extends Controller } /** - * @param AccountTaskerInterface $tasker - * @param ARI $repository - * @param Account $account + * @param Request $request + * @param JournalCollectorInterface $collector + * @param Account $account * - * @return View + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View */ - public function show(AccountTaskerInterface $tasker, ARI $repository, Account $account) + public function show(Request $request, JournalCollectorInterface $collector, Account $account) { if ($account->accountType->type === AccountType::INITIAL_BALANCE) { return $this->redirectToOriginalAccount($account); @@ -209,98 +241,95 @@ class AccountController extends Controller $subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type); $subTitle = $account->name; $range = Preferences::get('viewRange', '1M')->data; - /** @var Carbon $start */ - $start = session('start', Navigation::startOfPeriod(new Carbon, $range)); - /** @var Carbon $end */ - $end = session('end', Navigation::endOfPeriod(new Carbon, $range)); - $page = intval(Input::get('page')); - $pageSize = Preferences::get('transactionPageSize', 50)->data; - $offset = ($page - 1) * $pageSize; - $set = $tasker->getJournalsInPeriod(new Collection([$account]), [], $start, $end); - $count = $set->count(); - $subSet = $set->splice($offset, $pageSize); - $journals = new LengthAwarePaginator($subSet, $count, $pageSize, $page); + $start = session('start', Navigation::startOfPeriod(new Carbon, $range)); + $end = session('end', Navigation::endOfPeriod(new Carbon, $range)); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $chartUri = route('chart.account.single', [$account->id]); + $accountType = $account->accountType->type; + + // grab those journals: + $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->setLimit($pageSize)->setPage($page); + $journals = $collector->getPaginatedJournals(); $journals->setPath('accounts/show/' . $account->id); - // grouped other months thing: - // oldest transaction in account: - $start = $repository->oldestJournalDate($account); - $range = Preferences::get('viewRange', '1M')->data; - $start = Navigation::startOfPeriod($start, $range); - $end = Navigation::endOfX(new Carbon, $range); - $entries = new Collection; + // generate entries for each period (and cache those) + $entries = $this->periodEntries($account); - // chart properties for cache: - $cache = new CacheProperties; - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('account-show'); - $cache->addProperty($account->id); - - - if ($cache->has()) { - $entries = $cache->get(); - - return view('accounts.show', compact('account', 'what', 'entries', 'subTitleIcon', 'journals', 'subTitle')); - } - - // only include asset accounts when this account is an asset: - $assets = new Collection; - if (in_array($account->accountType->type, [AccountType::ASSET, AccountType::DEFAULT])) { - $assets = $repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]); - } - - while ($end >= $start) { - $end = Navigation::startOfPeriod($end, $range); - $currentEnd = Navigation::endOfPeriod($end, $range); - $spent = $tasker->amountOutInPeriod(new Collection([$account]), $assets, $end, $currentEnd); - $earned = $tasker->amountInInPeriod(new Collection([$account]), $assets, $end, $currentEnd); - $dateStr = $end->format('Y-m-d'); - $dateName = Navigation::periodShow($end, $range); - $entries->push([$dateStr, $dateName, $spent, $earned]); - $end = Navigation::subtractPeriod($end, $range, 1); - - } - $cache->store($entries); - - return view('accounts.show', compact('account', 'what', 'entries', 'subTitleIcon', 'journals', 'subTitle')); + return view('accounts.show', compact('account', 'accountType', 'entries', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); } /** - * @param AccountTaskerInterface $tasker - * @param Account $account - * @param string $date + * @param Request $request + * @param AccountRepositoryInterface $repository + * @param Account $account * * @return View */ - public function showWithDate(AccountTaskerInterface $tasker, Account $account, string $date) + public function showAll(Request $request, AccountRepositoryInterface $repository, Account $account) { - $carbon = new Carbon($date); - $range = Preferences::get('viewRange', '1M')->data; - $start = Navigation::startOfPeriod($carbon, $range); - $end = Navigation::endOfPeriod($carbon, $range); - $subTitle = $account->name . ' (' . Navigation::periodShow($start, $range) . ')'; - $page = intval(Input::get('page')); - $page = $page === 0 ? 1 : $page; - $pageSize = Preferences::get('transactionPageSize', 50)->data; - $offset = ($page - 1) * $pageSize; - $set = $tasker->getJournalsInPeriod(new Collection([$account]), [], $start, $end); - $count = $set->count(); - $subSet = $set->splice($offset, $pageSize); - $journals = new LengthAwarePaginator($subSet, $count, $pageSize, $page); + $subTitle = sprintf('%s (%s)', $account->name, strtolower(trans('firefly.everything'))); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $chartUri = route('chart.account.all', [$account->id]); + + // replace with journal collector: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser(auth()->user()); + $collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page); + $journals = $collector->getPaginatedJournals(); + $journals->setPath('accounts/show/' . $account->id . '/all'); + + // get oldest and newest journal for account: + $start = $repository->oldestJournalDate($account); + $end = $repository->newestJournalDate($account); + + // same call, except "entries". + return view('accounts.show', compact('account', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); + } + + /** + * @param Request $request + * @param Account $account + * @param string $date + * + * @return View + */ + public function showByDate(Request $request, Account $account, string $date) + { + $carbon = new Carbon($date); + $range = Preferences::get('viewRange', '1M')->data; + $start = Navigation::startOfPeriod($carbon, $range); + $end = Navigation::endOfPeriod($carbon, $range); + $subTitle = $account->name . ' (' . Navigation::periodShow($start, $range) . ')'; + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $chartUri = route('chart.account.period', [$account->id, $carbon->format('Y-m-d')]); + $accountType = $account->accountType->type; + + // replace with journal collector: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->setLimit($pageSize)->setPage($page); + $journals = $collector->getPaginatedJournals(); $journals->setPath('accounts/show/' . $account->id . '/' . $date); - return view('accounts.show_with_date', compact('category', 'date', 'account', 'journals', 'subTitle', 'carbon')); + // generate entries for each period (and cache those) + $entries = $this->periodEntries($account); + + // same call, except "entries". + return view('accounts.show', compact('account', 'accountType', 'entries', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); } /** * @param AccountFormRequest $request - * @param ARI $repository + * @param AccountRepositoryInterface $repository * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * */ - public function store(AccountFormRequest $request, ARI $repository) + public function store(AccountFormRequest $request, AccountRepositoryInterface $repository) { $data = $request->getAccountData(); $account = $repository->store($data); @@ -315,7 +344,7 @@ class AccountController extends Controller Preferences::set('frontPageAccounts', $frontPage); } - if (intval(Input::get('create_another')) === 1) { + if (intval($request->get('create_another')) === 1) { // set value so create routine will not overwrite URL: Session::put('accounts.create.fromStore', true); @@ -323,17 +352,17 @@ class AccountController extends Controller } // redirect to previous URL. - return redirect(session('accounts.create.url')); + return redirect($this->getPreviousUri('accounts.create.uri')); } /** * @param AccountFormRequest $request - * @param ARI $repository + * @param AccountRepositoryInterface $repository * @param Account $account * * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function update(AccountFormRequest $request, ARI $repository, Account $account) + public function update(AccountFormRequest $request, AccountRepositoryInterface $repository, Account $account) { $data = $request->getAccountData(); $repository->update($account, $data); @@ -341,7 +370,7 @@ class AccountController extends Controller Session::flash('success', strval(trans('firefly.updated_account', ['name' => $account->name]))); Preferences::mark(); - if (intval(Input::get('return_to_edit')) === 1) { + if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: Session::put('accounts.edit.fromUpdate', true); @@ -349,7 +378,7 @@ class AccountController extends Controller } // redirect to previous URL. - return redirect(session('accounts.edit.url')); + return redirect($this->getPreviousUri('accounts.edit.uri')); } @@ -366,7 +395,64 @@ class AccountController extends Controller return $array[$entryId]; } - return ''; + return '0'; + } + + /** + * This method returns "period entries", so nov-2015, dec-2015, etc etc (this depends on the users session range) + * and for each period, the amount of money spent and earned. This is a complex operation which is cached for + * performance reasons. + * + * @param Account $account The account involved. + * + * @return Collection + */ + private function periodEntries(Account $account): Collection + { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + /** @var AccountTaskerInterface $tasker */ + $tasker = app(AccountTaskerInterface::class); + + $start = $repository->oldestJournalDate($account); + $range = Preferences::get('viewRange', '1M')->data; + $start = Navigation::startOfPeriod($start, $range); + $end = Navigation::endOfX(new Carbon, $range); + $entries = new Collection; + + // properties for cache + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('account-show-period-entries'); + $cache->addProperty($account->id); + + if ($cache->has()) { + Log::debug('Entries are cached, return cache.'); + + return $cache->get(); + } + + // only include asset accounts when this account is an asset: + $assets = new Collection; + if (in_array($account->accountType->type, [AccountType::ASSET, AccountType::DEFAULT])) { + $assets = $repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]); + } + Log::debug('Going to get period expenses and incomes.'); + while ($end >= $start) { + $end = Navigation::startOfPeriod($end, $range); + $currentEnd = Navigation::endOfPeriod($end, $range); + $spent = $tasker->amountOutInPeriod(new Collection([$account]), $assets, $end, $currentEnd); + $earned = $tasker->amountInInPeriod(new Collection([$account]), $assets, $end, $currentEnd); + $dateStr = $end->format('Y-m-d'); + $dateName = Navigation::periodShow($end, $range); + $entries->push([$dateStr, $dateName, $spent, $earned, clone $end]); + $end = Navigation::subtractPeriod($end, $range, 1); + + } + $cache->store($entries); + + return $entries; } /** diff --git a/app/Http/Controllers/Admin/ConfigurationController.php b/app/Http/Controllers/Admin/ConfigurationController.php index 147dfe31c6..909d33ae62 100644 --- a/app/Http/Controllers/Admin/ConfigurationController.php +++ b/app/Http/Controllers/Admin/ConfigurationController.php @@ -14,7 +14,6 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers\Admin; -use Config; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\ConfigurationRequest; use FireflyIII\Support\Facades\FireflyConfig; @@ -37,8 +36,15 @@ class ConfigurationController extends Controller { parent::__construct(); - View::share('title', strval(trans('firefly.administration'))); - View::share('mainTitleIcon', 'fa-hand-spock-o'); + + $this->middleware( + function ($request, $next) { + View::share('title', strval(trans('firefly.administration'))); + View::share('mainTitleIcon', 'fa-hand-spock-o'); + + return $next($request); + } + ); } @@ -52,9 +58,14 @@ class ConfigurationController extends Controller // all available configuration and their default value in case // they don't exist yet. - $singleUserMode = FireflyConfig::get('single_user_mode', Config::get('firefly.configuration.single_user_mode'))->data; + $singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data; + $isDemoSite = FireflyConfig::get('is_demo_site', config('firefly.configuration.is_demo_site'))->data; + $siteOwner = env('SITE_OWNER'); - return view('admin.configuration.index', compact('subTitle', 'subTitleIcon', 'singleUserMode')); + return view( + 'admin.configuration.index', + compact('subTitle', 'subTitleIcon', 'singleUserMode', 'isDemoSite', 'siteOwner') + ); } @@ -63,13 +74,14 @@ class ConfigurationController extends Controller * * @return \Illuminate\Http\RedirectResponse */ - public function store(ConfigurationRequest $request) + public function postIndex(ConfigurationRequest $request) { // get config values: $data = $request->getConfigurationData(); // store config values FireflyConfig::set('single_user_mode', $data['single_user_mode']); + FireflyConfig::set('is_demo_site', $data['is_demo_site']); // flash message Session::flash('success', strval(trans('firefly.configuration_updated'))); diff --git a/app/Http/Controllers/Admin/DomainController.php b/app/Http/Controllers/Admin/DomainController.php deleted file mode 100644 index 2115d5816d..0000000000 --- a/app/Http/Controllers/Admin/DomainController.php +++ /dev/null @@ -1,140 +0,0 @@ -data; - - // known domains - $knownDomains = $this->getKnownDomains(); - - return view('admin.domains.index', compact('title', 'mainTitleIcon', 'knownDomains', 'subTitle', 'subTitleIcon', 'domains')); - } - - - /** - * @param Request $request - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - */ - public function manual(Request $request) - { - if (strlen($request->get('domain')) === 0) { - Session::flash('error', trans('firefly.no_domain_filled_in')); - - return redirect(route('admin.users.domains')); - } - - $domain = strtolower($request->get('domain')); - $blocked = FireflyConfig::get('blocked-domains', [])->data; - - if (in_array($domain, $blocked)) { - Session::flash('error', trans('firefly.domain_already_blocked', ['domain' => $domain])); - - return redirect(route('admin.users.domains')); - } - $blocked[] = $domain; - FireflyConfig::set('blocked-domains', $blocked); - - Session::flash('success', trans('firefly.domain_is_now_blocked', ['domain' => $domain])); - - return redirect(route('admin.users.domains')); - } - - /** - * @param string $domain - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - */ - public function toggleDomain(string $domain) - { - $domain = strtolower($domain); - $blocked = FireflyConfig::get('blocked-domains', [])->data; - - if (in_array($domain, $blocked)) { - $key = array_search($domain, $blocked); - unset($blocked[$key]); - sort($blocked); - - FireflyConfig::set('blocked-domains', $blocked); - Session::flash('message', trans('firefly.domain_now_unblocked', ['domain' => $domain])); - - - return redirect(route('admin.users.domains')); - - } - - $blocked[] = $domain; - - FireflyConfig::set('blocked-domains', $blocked); - Session::flash('message', trans('firefly.domain_now_blocked', ['domain' => $domain])); - - return redirect(route('admin.users.domains')); - } - - /** - * @return array - */ - private function getKnownDomains(): array - { - /** @var UserRepositoryInterface $repository */ - $repository = app(UserRepositoryInterface::class); - $users = $repository->all(); - $set = []; - $filtered = []; - /** @var User $user */ - foreach ($users as $user) { - $email = $user->email; - $parts = explode('@', $email); - $set[] = strtolower($parts[1]); - } - $set = array_unique($set); - // filter for already banned domains: - $blocked = FireflyConfig::get('blocked-domains', [])->data; - - foreach ($set as $domain) { - // in the block array? ignore it. - if (!in_array($domain, $blocked)) { - $filtered[] = $domain; - } - } - - return $filtered; - } -} diff --git a/app/Http/Controllers/Admin/HomeController.php b/app/Http/Controllers/Admin/HomeController.php index d2d32751fb..fa798ef248 100644 --- a/app/Http/Controllers/Admin/HomeController.php +++ b/app/Http/Controllers/Admin/HomeController.php @@ -24,7 +24,7 @@ use FireflyIII\Http\Controllers\Controller; class HomeController extends Controller { /** - * @return mixed + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function index() { diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 5557539f35..818c12c2f6 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -15,9 +15,12 @@ namespace FireflyIII\Http\Controllers\Admin; use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Http\Requests\UserFormRequest; use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Preferences; +use Session; +use View; /** * Class UserController @@ -26,55 +29,75 @@ use Preferences; */ class UserController extends Controller { + /** + * + */ + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + View::share('title', strval(trans('firefly.administration'))); + View::share('mainTitleIcon', 'fa-hand-spock-o'); + + return $next($request); + } + ); + } + /** * @param User $user * - * @return int + * @return View */ public function edit(User $user) { - return $user->id; + // put previous url in session if not redirect from store (not "return_to_edit"). + if (session('users.edit.fromUpdate') !== true) { + $this->rememberPreviousUri('users.edit.uri'); + } + Session::forget('users.edit.fromUpdate'); + + $subTitle = strval(trans('firefly.edit_user', ['email' => $user->email])); + $subTitleIcon = 'fa-user-o'; + $codes = [ + '' => strval(trans('firefly.no_block_code')), + 'bounced' => strval(trans('firefly.block_code_bounced')), + 'expired' => strval(trans('firefly.block_code_expired')), + ]; + + return view('admin.users.edit', compact('user', 'subTitle', 'subTitleIcon', 'codes')); } /** * @param UserRepositoryInterface $repository * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @return View */ public function index(UserRepositoryInterface $repository) { - $title = strval(trans('firefly.administration')); - $mainTitleIcon = 'fa-hand-spock-o'; - $subTitle = strval(trans('firefly.user_administration')); - $subTitleIcon = 'fa-users'; - $confirmAccount = env('MUST_CONFIRM_ACCOUNT', false); - $users = $repository->all(); + $subTitle = strval(trans('firefly.user_administration')); + $subTitleIcon = 'fa-users'; + $users = $repository->all(); // add meta stuff. $users->each( - function (User $user) use ($confirmAccount) { - // is user activated? - $isConfirmed = Preferences::getForUser($user, 'user_confirmed', false)->data; - $user->activated = true; - if ($isConfirmed === false && $confirmAccount === true) { - $user->activated = false; - } - + function (User $user) { + $list = ['twoFactorAuthEnabled', 'twoFactorAuthSecret']; + $preferences = Preferences::getArrayForUser($user, $list); $user->isAdmin = $user->hasRole('owner'); - $is2faEnabled = Preferences::getForUser($user, 'twoFactorAuthEnabled', false)->data; - $has2faSecret = !is_null(Preferences::getForUser($user, 'twoFactorAuthSecret')); - $user->has2FA = false; - if ($is2faEnabled && $has2faSecret) { - $user->has2FA = true; - } + $is2faEnabled = $preferences['twoFactorAuthEnabled'] === true; + $has2faSecret = !is_null($preferences['twoFactorAuthSecret']); + $user->has2FA = ($is2faEnabled && $has2faSecret) ? true : false; + $user->prefs = $preferences; } ); - return view('admin.users.index', compact('title', 'mainTitleIcon', 'subTitle', 'subTitleIcon', 'users')); + return view('admin.users.index', compact('subTitle', 'subTitleIcon', 'users')); } @@ -90,40 +113,51 @@ class UserController extends Controller $mainTitleIcon = 'fa-hand-spock-o'; $subTitle = strval(trans('firefly.single_user_administration', ['email' => $user->email])); $subTitleIcon = 'fa-user'; - - // get IP info: - $defaultIp = '0.0.0.0'; - $regPref = Preferences::getForUser($user, 'registration_ip_address'); - $registration = $defaultIp; - $conPref = Preferences::getForUser($user, 'confirmation_ip_address'); - $confirmation = $defaultIp; - if (!is_null($regPref)) { - $registration = $regPref->data; - } - if (!is_null($conPref)) { - $confirmation = $conPref->data; - } - - $registrationHost = ''; - $confirmationHost = ''; - - if ($registration != $defaultIp) { - $registrationHost = gethostbyaddr($registration); - } - if ($confirmation != $defaultIp) { - $confirmationHost = gethostbyaddr($confirmation); - } - - $information = $repository->getUserData($user); + $information = $repository->getUserData($user); return view( 'admin.users.show', compact( - 'title', 'mainTitleIcon', 'subTitle', 'subTitleIcon', 'information', - 'user', 'registration', 'confirmation', 'registrationHost', 'confirmationHost' + 'title', 'mainTitleIcon', 'subTitle', 'subTitleIcon', 'information', 'user' ) ); } + /** + * @param UserFormRequest $request + * @param User $user + * + * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function update(UserFormRequest $request, User $user) + { + $data = $request->getUserData(); + + // update password + if (strlen($data['password']) > 0) { + $user->password = bcrypt($data['password']); + $user->save(); + } + + // change blocked status and code: + $user->blocked = $data['blocked']; + $user->blocked_code = $data['blocked_code']; + $user->save(); + + Session::flash('success', strval(trans('firefly.updated_user', ['email' => $user->email]))); + Preferences::mark(); + + if (intval($request->get('return_to_edit')) === 1) { + // set value so edit routine will not overwrite URL: + Session::put('users.edit.fromUpdate', true); + + return redirect(route('admin.users.edit', [$user->id]))->withInput(['return_to_edit' => 1]); + } + + // redirect to previous URL. + return redirect($this->getPreviousUri('users.edit.uri')); + + } + } diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php index 05e71a9a8f..a6532309f1 100644 --- a/app/Http/Controllers/AttachmentController.php +++ b/app/Http/Controllers/AttachmentController.php @@ -13,24 +13,22 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers; -use Crypt; use File; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Requests\AttachmentFormRequest; use FireflyIII\Models\Attachment; use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface; -use Input; -use Log; +use Illuminate\Http\Response as LaravelResponse; use Preferences; use Response; use Session; -use Storage; -use URL; use View; /** * Class AttachmentController * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) // it's 13. + * * @package FireflyIII\Http\Controllers */ class AttachmentController extends Controller @@ -42,8 +40,16 @@ class AttachmentController extends Controller public function __construct() { parent::__construct(); - View::share('mainTitleIcon', 'fa-paperclip'); - View::share('title', trans('firefly.attachments')); + + // translations: + $this->middleware( + function ($request, $next) { + View::share('mainTitleIcon', 'fa-paperclip'); + View::share('title', trans('firefly.attachments')); + + return $next($request); + } + ); } /** @@ -56,7 +62,7 @@ class AttachmentController extends Controller $subTitle = trans('firefly.delete_attachment', ['name' => $attachment->filename]); // put previous url in session - Session::put('attachments.delete.url', URL::previous()); + $this->rememberPreviousUri('attachments.delete.uri'); Session::flash('gaEventCategory', 'attachments'); Session::flash('gaEventAction', 'delete-attachment'); @@ -78,29 +84,25 @@ class AttachmentController extends Controller Session::flash('success', strval(trans('firefly.attachment_deleted', ['name' => $name]))); Preferences::mark(); - return redirect(session('attachments.delete.url')); + return redirect($this->getPreviousUri('attachments.delete.uri')); } /** - * @param Attachment $attachment + * @param AttachmentRepositoryInterface $repository + * @param Attachment $attachment * + * @return mixed * @throws FireflyException - * */ - public function download(Attachment $attachment) + public function download(AttachmentRepositoryInterface $repository, Attachment $attachment) { - // create a disk. - $disk = Storage::disk('upload'); - $file = $attachment->fileName(); - - if ($disk->exists($file)) { - + if ($repository->exists($attachment)) { + $content = $repository->getContent($attachment); $quoted = sprintf('"%s"', addcslashes(basename($attachment->filename), '"\\')); - $content = Crypt::decrypt($disk->get($file)); - Log::debug('Send file to user', ['file' => $quoted, 'size' => strlen($content)]); - - return response($content, 200) + /** @var LaravelResponse $response */ + $response = response($content, 200); + $response ->header('Content-Description', 'File Transfer') ->header('Content-Type', 'application/octet-stream') ->header('Content-Disposition', 'attachment; filename=' . $quoted) @@ -111,6 +113,7 @@ class AttachmentController extends Controller ->header('Pragma', 'public') ->header('Content-Length', strlen($content)); + return $response; } throw new FireflyException('Could not find the indicated attachment. The file is no longer there.'); } @@ -127,7 +130,7 @@ class AttachmentController extends Controller // put previous url in session if not redirect from store (not "return_to_edit"). if (session('attachments.edit.fromUpdate') !== true) { - Session::put('attachments.edit.url', URL::previous()); + $this->rememberPreviousUri('attachments.edit.uri'); } Session::forget('attachments.edit.fromUpdate'); @@ -143,7 +146,6 @@ class AttachmentController extends Controller { $image = 'images/page_green.png'; - if ($attachment->mime == 'application/pdf') { $image = 'images/page_white_acrobat.png'; } @@ -170,7 +172,7 @@ class AttachmentController extends Controller Session::flash('success', strval(trans('firefly.attachment_updated', ['name' => $attachment->filename]))); Preferences::mark(); - if (intval(Input::get('return_to_edit')) === 1) { + if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: Session::put('attachments.edit.fromUpdate', true); @@ -178,7 +180,7 @@ class AttachmentController extends Controller } // redirect to previous URL. - return redirect(session('attachments.edit.url')); + return redirect($this->getPreviousUri('attachments.edit.uri')); } diff --git a/app/Http/Controllers/Auth/ConfirmationController.php b/app/Http/Controllers/Auth/ConfirmationController.php deleted file mode 100644 index 3221971227..0000000000 --- a/app/Http/Controllers/Auth/ConfirmationController.php +++ /dev/null @@ -1,90 +0,0 @@ -data; - $time = Preferences::get('user_confirmed_last_mail', 0)->data; - $now = time(); - $maxDiff = config('firefly.confirmation_age'); - - if ($database === $code && ($now - $time <= $maxDiff)) { - - // trigger user registration event: - event(new ConfirmedUser(auth()->user(), $request->ip())); - - Preferences::setForUser(auth()->user(), 'user_confirmed', true); - Preferences::setForUser(auth()->user(), 'user_confirmed_confirmed', time()); - Session::flash('success', strval(trans('firefly.account_is_confirmed'))); - - return redirect(route('home')); - } - throw new FireflyException(trans('firefly.invalid_activation_code')); - } - - /** - * @param Request $request - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - */ - public function resendConfirmation(Request $request) - { - $time = Preferences::get('user_confirmed_last_mail', 0)->data; - $now = time(); - $maxDiff = config('firefly.resend_confirmation'); - $owner = env('SITE_OWNER', 'mail@example.com'); - $view = 'auth.confirmation.no-resent'; - if ($now - $time > $maxDiff) { - event(new ResentConfirmation(auth()->user(), $request->ip())); - $view = 'auth.confirmation.resent'; - } - - return view($view, ['owner' => $owner]); - } - -} diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php old mode 100755 new mode 100644 index 7d6e12b3fc..3942c0868b --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -13,7 +13,10 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers\Auth; use FireflyIII\Http\Controllers\Controller; +use FireflyIII\User; use Illuminate\Foundation\Auth\SendsPasswordResetEmails; +use Illuminate\Http\Request; +use Password; /** * Class ForgotPasswordController @@ -33,4 +36,39 @@ class ForgotPasswordController extends Controller parent::__construct(); $this->middleware('guest'); } + + /** + * Send a reset link to the given user. + * + * @param Request $request + * + * @return \Illuminate\Http\RedirectResponse + */ + public function sendResetLinkEmail(Request $request) + { + $this->validate($request, ['email' => 'required|email']); + + // verify if the user is not a demo user. If so, we give him back an error. + $user = User::where('email', $request->get('email'))->first(); + if (!is_null($user) && $user->hasRole('demo')) { + return back()->withErrors( + ['email' => trans('firefly.cannot_reset_demo_user')] + ); + } + + $response = $this->broker()->sendResetLink( + $request->only('email') + ); + + if ($response === Password::RESET_LINK_SENT) { + return back()->with('status', trans($response)); + } + + // If an error was returned by the password broker, we will get this message + // translated so we can notify a user of the problem. We'll redirect back + // to where the users came from so they can attempt this process again. + return back()->withErrors( + ['email' => trans($response)] + ); + } } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php old mode 100755 new mode 100644 index e805c6fc6c..04e8abcc06 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -18,11 +18,7 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\User; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; -use Illuminate\Mail\Message; use Lang; -use Log; -use Mail; -use Swift_TransportException; /** * Class LoginController @@ -31,26 +27,9 @@ use Swift_TransportException; */ class LoginController extends Controller { - /* - |-------------------------------------------------------------------------- - | Login Controller - |-------------------------------------------------------------------------- - | - | This controller handles authenticating users for the application and - | redirecting them to your home screen. The controller uses a trait - | to conveniently provide its functionality to your applications. - | - */ use AuthenticatesUsers; - /** - * Where to redirect users after login / registration. - * - * @var string - */ - protected $redirectTo = '/home'; - /** * Create a new controller instance. * @@ -64,44 +43,29 @@ class LoginController extends Controller /** * Handle a login request to the application. * - * @param \Illuminate\Http\Request $request + * @param Request $request * - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response */ public function login(Request $request) { $this->validateLogin($request); - - // If the class is using the ThrottlesLogins trait, we can automatically throttle - // the login attempts for this application. We'll key this by the username and - // the IP address of the client making these requests into this application. - if ($lockedOut = $this->hasTooManyLoginAttempts($request)) { + $lockedOut = $this->hasTooManyLoginAttempts($request); + if ($lockedOut) { $this->fireLockoutEvent($request); return $this->sendLockoutResponse($request); } $credentials = $this->credentials($request); - $credentials['blocked'] = 0; // most not be blocked. + $credentials['blocked'] = 0; // must not be blocked. if ($this->guard()->attempt($credentials, $request->has('remember'))) { return $this->sendLoginResponse($request); } - // check if user is blocked: - $errorMessage = ''; - /** @var User $foundUser */ - $foundUser = User::where('email', $credentials['email'])->where('blocked', 1)->first(); - if (!is_null($foundUser)) { - // if it exists, show message: - $code = strlen(strval($foundUser->blocked_code)) > 0 ? $foundUser->blocked_code : 'general_blocked'; - $errorMessage = strval(trans('firefly.' . $code . '_error', ['email' => $credentials['email']])); - $this->reportBlockedUserLoginAttempt($foundUser, $code, $request->ip()); - } + $errorMessage = $this->getBlockedError($credentials['email']); - // If the login attempt was unsuccessful we will increment the number of attempts - // to login and redirect the user back to the login form. Of course, when this - // user surpasses their maximum number of attempts they will get locked out. if (!$lockedOut) { $this->incrementLoginAttempts($request); } @@ -109,6 +73,34 @@ class LoginController extends Controller return $this->sendFailedLoginResponse($request, $errorMessage); } + /** + * @param Request $request + * + * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function logout(Request $request) + { + if (intval(getenv('SANDSTORM')) === 1) { + return view('error')->with('message', strval(trans('firefly.sandstorm_not_available'))); + } + + $this->guard()->logout(); + + $request->session()->flush(); + + $request->session()->regenerate(); + + return redirect('/'); + } + + /** + * @return string + */ + public function redirectTo(): string + { + return route('index'); + } + /** * Show the application login form. * @@ -168,32 +160,22 @@ class LoginController extends Controller } /** - * Send a message home about the blocked attempt to login. - * Perhaps in a later stage, simply log these messages. + * @param string $email * - * @param User $user - * @param string $code - * @param string $ipAddress + * @return string */ - private function reportBlockedUserLoginAttempt(User $user, string $code, string $ipAddress) + private function getBlockedError(string $email): string { - - try { - $email = env('SITE_OWNER', false); - $fields = [ - 'user_id' => $user->id, - 'user_address' => $user->email, - 'code' => $code, - 'ip' => $ipAddress, - ]; - - Mail::send( - ['emails.blocked-login-html', 'emails.blocked-login'], $fields, function (Message $message) use ($email, $user) { - $message->to($email, $email)->subject('Blocked a login attempt from ' . trim($user->email) . '.'); - } - ); - } catch (Swift_TransportException $e) { - Log::error($e->getMessage()); + // check if user is blocked: + $errorMessage = ''; + /** @var User $foundUser */ + $foundUser = User::where('email', $email)->where('blocked', 1)->first(); + if (!is_null($foundUser)) { + // user exists, but is blocked: + $code = strlen(strval($foundUser->blocked_code)) > 0 ? $foundUser->blocked_code : 'general_blocked'; + $errorMessage = strval(trans('firefly.' . $code . '_error', ['email' => $email])); } + + return $errorMessage; } } diff --git a/app/Http/Controllers/Auth/PasswordController.php b/app/Http/Controllers/Auth/PasswordController.php index 95a5c82132..06a6fa99b2 100644 --- a/app/Http/Controllers/Auth/PasswordController.php +++ b/app/Http/Controllers/Auth/PasswordController.php @@ -26,21 +26,11 @@ use Illuminate\Support\Facades\Password; * * @package FireflyIII\Http\Controllers\Auth * @method getEmailSubject() - * @method getSendResetLinkEmailSuccessResponse() - * @method getSendResetLinkEmailFailureResponse() + * @method getSendResetLinkEmailSuccessResponse(string $response) + * @method getSendResetLinkEmailFailureResponse(string $response) */ class PasswordController extends Controller { - /* - |-------------------------------------------------------------------------- - | Password Reset Controller - |-------------------------------------------------------------------------- - | - | This controller is responsible for handling password reset requests - | and uses a simple trait to include this behavior. You're free to - | explore this trait and override any methods you wish to tweak. - | - */ use ResetsPasswords; @@ -57,6 +47,7 @@ class PasswordController extends Controller /** * Send a reset link to the given user. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's 7 but ok * * @param \Illuminate\Http\Request $request * diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php old mode 100755 new mode 100644 index e7d6dc6692..2cf28042ba --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -14,17 +14,13 @@ namespace FireflyIII\Http\Controllers\Auth; use Auth; use Config; +use FireflyConfig; use FireflyIII\Events\RegisteredUser; use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Support\Facades\FireflyConfig; use FireflyIII\User; use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Http\Request; -use Illuminate\Mail\Message; -use Log; -use Mail; use Session; -use Swift_TransportException; use Validator; /** @@ -34,16 +30,6 @@ use Validator; */ class RegisterController extends Controller { - /* - |-------------------------------------------------------------------------- - | Register Controller - |-------------------------------------------------------------------------- - | - | This controller handles the registration of new users as well as their - | validation and creation. By default this controller uses a trait to - | provide this functionality without requiring any additional code. - | - */ use RegistersUsers; @@ -86,19 +72,6 @@ class RegisterController extends Controller $this->throwValidationException($request, $validator); } - $data = $request->all(); - $data['password'] = bcrypt($data['password']); - - // is user email domain blocked? - if ($this->isBlockedDomain($data['email'])) { - $validator->getMessageBag()->add('email', (string)trans('validation.invalid_domain')); - - $this->reportBlockedDomainRegistrationAttempt($data['email'], $request->ip()); - - $this->throwValidationException($request, $validator); - } - - $user = $this->create($request->all()); // trigger user registration event: @@ -123,7 +96,8 @@ class RegisterController extends Controller */ public function showRegistrationForm(Request $request) { - $showDemoWarning = config('firefly.show-demo-warning', false); + // is demo site? + $isDemoSite = FireflyConfig::get('is_demo_site', Config::get('firefly.configuration.is_demo_site'))->data; // is allowed to? $singleUserMode = FireflyConfig::get('single_user_mode', Config::get('firefly.configuration.single_user_mode'))->data; @@ -136,7 +110,7 @@ class RegisterController extends Controller $email = $request->old('email'); - return view('auth.register', compact('showDemoWarning', 'email')); + return view('auth.register', compact('isDemoSite', 'email')); } /** @@ -172,57 +146,4 @@ class RegisterController extends Controller ] ); } - - /** - * @return array - */ - private function getBlockedDomains() - { - return FireflyConfig::get('blocked-domains', [])->data; - } - - /** - * @param string $email - * - * @return bool - */ - private function isBlockedDomain(string $email) - { - $parts = explode('@', $email); - $blocked = $this->getBlockedDomains(); - - if (isset($parts[1]) && in_array($parts[1], $blocked)) { - return true; - } - - return false; - } - - /** - * Send a message home about a blocked domain and the address attempted to register. - * - * @param string $registrationMail - * @param string $ipAddress - */ - private function reportBlockedDomainRegistrationAttempt(string $registrationMail, string $ipAddress) - { - try { - $email = env('SITE_OWNER', false); - $parts = explode('@', $registrationMail); - $domain = $parts[1]; - $fields = [ - 'email_address' => $registrationMail, - 'blocked_domain' => $domain, - 'ip' => $ipAddress, - ]; - - Mail::send( - ['emails.blocked-registration-html', 'emails.blocked-registration'], $fields, function (Message $message) use ($email, $domain) { - $message->to($email, $email)->subject('Blocked a registration attempt with domain ' . $domain . '.'); - } - ); - } catch (Swift_TransportException $e) { - Log::error($e->getMessage()); - } - } } diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php old mode 100755 new mode 100644 index 3e8c162572..1d99a0c179 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -22,16 +22,6 @@ use Illuminate\Foundation\Auth\ResetsPasswords; */ class ResetPasswordController extends Controller { - /* - |-------------------------------------------------------------------------- - | Password Reset Controller - |-------------------------------------------------------------------------- - | - | This controller is responsible for handling password reset requests - | and uses a simple trait to include this behavior. You're free to - | explore this trait and override any methods you wish to tweak. - | - */ use ResetsPasswords; diff --git a/app/Http/Controllers/Auth/TwoFactorController.php b/app/Http/Controllers/Auth/TwoFactorController.php index d310cc3bd7..bf3b425161 100644 --- a/app/Http/Controllers/Auth/TwoFactorController.php +++ b/app/Http/Controllers/Auth/TwoFactorController.php @@ -17,6 +17,7 @@ use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\TokenFormRequest; +use Illuminate\Http\Request; use Log; use Preferences; use Session; @@ -30,21 +31,30 @@ class TwoFactorController extends Controller { /** - * @return mixed + * @param Request $request + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View * @throws FireflyException */ - public function index() + public function index(Request $request) { + $user = auth()->user(); // to make sure the validator in the next step gets the secret, we push it in session - $secret = Preferences::get('twoFactorAuthSecret', '')->data; + $secret = Preferences::get('twoFactorAuthSecret', null)->data; $title = strval(trans('firefly.two_factor_title')); - if (strlen($secret) === 0) { + // make sure the user has two factor configured: + $has2FA = Preferences::get('twoFactorAuthEnabled', null)->data; + if (is_null($has2FA) || $has2FA === false) { + return redirect(route('index')); + } + + if (strlen(strval($secret)) === 0) { throw new FireflyException('Your two factor authentication secret is empty, which it cannot be at this point. Please check the log files.'); } - Session::flash('two-factor-secret', $secret); + $request->session()->flash('two-factor-secret', $secret); return view('auth.two-factor', compact('user', 'title')); } @@ -70,6 +80,7 @@ class TwoFactorController extends Controller /** * @param TokenFormRequest $request + * @SuppressWarnings(PHPMD.UnusedFormalParameter) // it's unused but the class does some validation. * * @return mixed */ diff --git a/app/Http/Controllers/BillController.php b/app/Http/Controllers/BillController.php index d6095d8360..ae95ba23ef 100644 --- a/app/Http/Controllers/BillController.php +++ b/app/Http/Controllers/BillController.php @@ -14,11 +14,13 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers; use Carbon\Carbon; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Requests\BillFormRequest; use FireflyIII\Models\Bill; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Bill\BillRepositoryInterface; -use Input; +use Illuminate\Http\Request; +use Illuminate\Support\Collection; use Preferences; use Session; use URL; @@ -38,8 +40,16 @@ class BillController extends Controller public function __construct() { parent::__construct(); - View::share('title', trans('firefly.bills')); - View::share('mainTitleIcon', 'fa-calendar-o'); + + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.bills')); + View::share('mainTitleIcon', 'fa-calendar-o'); + + return $next($request); + } + ); } /** @@ -56,7 +66,7 @@ class BillController extends Controller // put previous url in session if not redirect from store (not "create another"). if (session('bills.create.fromStore') !== true) { - Session::put('bills.create.url', URL::previous()); + $this->rememberPreviousUri('bills.create.uri'); } Session::forget('bills.create.fromStore'); Session::flash('gaEventCategory', 'bills'); @@ -73,7 +83,7 @@ class BillController extends Controller public function delete(Bill $bill) { // put previous url in session - Session::put('bills.delete.url', URL::previous()); + $this->rememberPreviousUri('bills.delete.uri'); Session::flash('gaEventCategory', 'bills'); Session::flash('gaEventAction', 'delete'); $subTitle = trans('firefly.delete_bill', ['name' => $bill->name]); @@ -95,7 +105,7 @@ class BillController extends Controller Session::flash('success', strval(trans('firefly.deleted_bill', ['name' => $name]))); Preferences::mark(); - return redirect(session('bills.delete.url')); + return redirect($this->getPreviousUri('bills.delete.uri')); } /** @@ -113,7 +123,7 @@ class BillController extends Controller // put previous url in session if not redirect from store (not "return_to_edit"). if (session('bills.edit.fromUpdate') !== true) { - Session::put('bills.edit.url', URL::previous()); + $this->rememberPreviousUri('bills.edit.uri'); } Session::forget('bills.edit.fromUpdate'); Session::flash('gaEventCategory', 'bills'); @@ -140,7 +150,7 @@ class BillController extends Controller // paid in this period? $bill->paidDates = $repository->getPaidDatesInRange($bill, $start, $end); - $bill->payDates = $repository->getPayDatesInRange($bill, $start, $end); + $bill->payDates = $repository->getPayDatesInRange($bill, $start, $end); $lastDate = clone $start; if ($bill->paidDates->count() >= $bill->payDates->count()) { $lastDate = $end; @@ -180,22 +190,30 @@ class BillController extends Controller } /** + * @param Request $request * @param BillRepositoryInterface $repository * @param Bill $bill * * @return View */ - public function show(BillRepositoryInterface $repository, Bill $bill) + public function show(Request $request, BillRepositoryInterface $repository, Bill $bill) { /** @var Carbon $date */ $date = session('start'); $year = $date->year; - $page = intval(Input::get('page')) == 0 ? 1 : intval(Input::get('page')); + $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); - $journals = $repository->getJournals($bill, $page, $pageSize); $yearAverage = $repository->getYearAverage($bill, $date); $overallAverage = $repository->getOverallAverage($bill); + + // use collector: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setBills(new Collection([$bill]))->setLimit($pageSize)->setPage($page)->withBudgetInformation() + ->withCategoryInformation(); + $journals = $collector->getPaginatedJournals(); $journals->setPath('/bills/show/' . $bill->id); + $bill->nextExpectedMatch = $repository->nextExpectedMatch($bill, new Carbon); $hideBill = true; $subTitle = e($bill->name); @@ -216,7 +234,7 @@ class BillController extends Controller Session::flash('success', strval(trans('firefly.stored_new_bill', ['name' => e($bill->name)]))); Preferences::mark(); - if (intval(Input::get('create_another')) === 1) { + if (intval($request->get('create_another')) === 1) { // set value so create routine will not overwrite URL: Session::put('bills.create.fromStore', true); @@ -224,7 +242,7 @@ class BillController extends Controller } // redirect to previous URL. - return redirect(session('bills.create.url')); + return redirect($this->getPreviousUri('bills.create.uri')); } @@ -243,15 +261,14 @@ class BillController extends Controller Session::flash('success', strval(trans('firefly.updated_bill', ['name' => e($bill->name)]))); Preferences::mark(); - if (intval(Input::get('return_to_edit')) === 1) { + if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: Session::put('bills.edit.fromUpdate', true); return redirect(route('bills.edit', [$bill->id]))->withInput(['return_to_edit' => 1]); } - // redirect to previous URL. - return redirect(session('bills.edit.url')); + return redirect($this->getPreviousUri('bills.edit.uri')); } diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index 439dfb6d8b..f21bc904fa 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -15,22 +15,21 @@ namespace FireflyIII\Http\Controllers; use Amount; use Carbon\Carbon; -use Config; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Requests\BudgetFormRequest; +use FireflyIII\Http\Requests\BudgetIncomeRequest; use FireflyIII\Models\AccountType; use FireflyIII\Models\Budget; -use FireflyIII\Models\LimitRepetition; +use FireflyIII\Models\BudgetLimit; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; -use Illuminate\Pagination\LengthAwarePaginator; +use FireflyIII\Support\CacheProperties; +use Illuminate\Http\Request; use Illuminate\Support\Collection; -use Input; -use Navigation; use Preferences; use Response; use Session; -use URL; use View; /** @@ -41,44 +40,49 @@ use View; class BudgetController extends Controller { + /** @var BudgetRepositoryInterface */ + private $repository; + /** * */ public function __construct() { parent::__construct(); - View::share('title', trans('firefly.budgets')); - View::share('mainTitleIcon', 'fa-tasks'); + View::share('hideBudgets', true); + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.budgets')); + View::share('mainTitleIcon', 'fa-tasks'); + $this->repository = app(BudgetRepositoryInterface::class); + + return $next($request); + } + ); } /** - * @param BudgetRepositoryInterface $repository - * @param Budget $budget + * @param Request $request + * @param Budget $budget * - * @return \Symfony\Component\HttpFoundation\Response + * @return \Illuminate\Http\JsonResponse */ - public function amount(BudgetRepositoryInterface $repository, Budget $budget) + public function amount(Request $request, Budget $budget) { - $amount = intval(Input::get('amount')); + $amount = intval($request->get('amount')); /** @var Carbon $start */ $start = session('start', Carbon::now()->startOfMonth()); /** @var Carbon $end */ - $end = session('end', Carbon::now()->endOfMonth()); - $viewRange = Preferences::get('viewRange', '1M')->data; - - // is custom view range? - if (session('is_custom_range') === true) { - $viewRange = 'custom'; - } - - $limitRepetition = $repository->updateLimitAmount($budget, $start, $end, $viewRange, $amount); + $end = session('end', Carbon::now()->endOfMonth()); + $budgetLimit = $this->repository->updateLimitAmount($budget, $start, $end, $amount); if ($amount == 0) { - $limitRepetition = null; + $budgetLimit = null; } Preferences::mark(); - return Response::json(['name' => $budget->name, 'repetition' => $limitRepetition ? $limitRepetition->id : 0]); + return Response::json(['name' => $budget->name, 'limit' => $budgetLimit ? $budgetLimit->id : 0, 'amount' => $amount]); } @@ -89,7 +93,7 @@ class BudgetController extends Controller { // put previous url in session if not redirect from store (not "create another"). if (session('budgets.create.fromStore') !== true) { - Session::put('budgets.create.url', URL::previous()); + $this->rememberPreviousUri('budgets.create.uri'); } Session::forget('budgets.create.fromStore'); Session::flash('gaEventCategory', 'budgets'); @@ -109,7 +113,7 @@ class BudgetController extends Controller $subTitle = trans('firefly.delete_budget', ['name' => $budget->name]); // put previous url in session - Session::put('budgets.delete.url', URL::previous()); + $this->rememberPreviousUri('budgets.delete.uri'); Session::flash('gaEventCategory', 'budgets'); Session::flash('gaEventAction', 'delete'); @@ -117,23 +121,19 @@ class BudgetController extends Controller } /** - * @param Budget $budget - * @param BudgetRepositoryInterface $repository + * @param Budget $budget * * @return \Illuminate\Http\RedirectResponse */ - public function destroy(Budget $budget, BudgetRepositoryInterface $repository) + public function destroy(Budget $budget) { $name = $budget->name; - $repository->destroy($budget); - - + $this->repository->destroy($budget); Session::flash('success', strval(trans('firefly.deleted_budget', ['name' => e($name)]))); Preferences::mark(); - - return redirect(session('budgets.delete.url')); + return redirect($this->getPreviousUri('budgets.delete.uri')); } /** @@ -147,7 +147,7 @@ class BudgetController extends Controller // put previous url in session if not redirect from store (not "return_to_edit"). if (session('budgets.edit.fromUpdate') !== true) { - Session::put('budgets.edit.url', URL::previous()); + $this->rememberPreviousUri('budgets.edit.uri'); } Session::forget('budgets.edit.fromUpdate'); Session::flash('gaEventCategory', 'budgets'); @@ -158,250 +158,188 @@ class BudgetController extends Controller } /** - * @param BudgetRepositoryInterface $repository - * @param AccountRepositoryInterface $accountRepository - * * @return View - * */ - public function index(BudgetRepositoryInterface $repository, AccountRepositoryInterface $accountRepository) + public function index() { - $repository->cleanupBudgets(); + $this->repository->cleanupBudgets(); - $budgets = $repository->getActiveBudgets(); - $inactive = $repository->getInactiveBudgets(); - $spent = '0'; - $budgeted = '0'; - $range = Preferences::get('viewRange', '1M')->data; - $repeatFreq = Config::get('firefly.range_to_repeat_freq.' . $range); - - if (session('is_custom_range') === true) { - $repeatFreq = 'custom'; - } - - /** @var Carbon $start */ - $start = session('start', new Carbon); - /** @var Carbon $end */ + $budgets = $this->repository->getActiveBudgets(); + $inactive = $this->repository->getInactiveBudgets(); + $start = session('start', new Carbon); $end = session('end', new Carbon); - $key = 'budgetIncomeTotal' . $start->format('Ymd') . $end->format('Ymd'); - $budgetIncomeTotal = Preferences::get($key, 1000)->data; - $period = Navigation::periodShow($start, $range); $periodStart = $start->formatLocalized($this->monthAndDayFormat); $periodEnd = $end->formatLocalized($this->monthAndDayFormat); - $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH]); - $startAsString = $start->format('Y-m-d'); - $endAsString = $end->format('Y-m-d'); - - // loop the budgets: - /** @var Budget $budget */ - foreach ($budgets as $budget) { - $budget->spent = $repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end); - $allRepetitions = $repository->getAllBudgetLimitRepetitions($start, $end); - $otherRepetitions = new Collection; - - /** @var LimitRepetition $repetition */ - foreach ($allRepetitions as $repetition) { - if ($repetition->budget_id == $budget->id) { - if ($repetition->budgetLimit->repeat_freq == $repeatFreq - && $repetition->startdate->format('Y-m-d') == $startAsString - && $repetition->enddate->format('Y-m-d') == $endAsString - ) { - // do something - $budget->currentRep = $repetition; - continue; - } - $otherRepetitions->push($repetition); - } - } - $budget->otherRepetitions = $otherRepetitions; - - if (!is_null($budget->currentRep) && !is_null($budget->currentRep->id)) { - $budgeted = bcadd($budgeted, $budget->currentRep->amount); - } - $spent = bcadd($spent, $budget->spent); - - } - - - $defaultCurrency = Amount::getDefaultCurrency(); + $budgetInformation = $this->collectBudgetInformation($budgets, $start, $end); + $defaultCurrency = Amount::getDefaultCurrency(); + $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end); + $spent = array_sum(array_column($budgetInformation, 'spent')); + $budgeted = array_sum(array_column($budgetInformation, 'budgeted')); return view( - 'budgets.index', compact( - 'periodStart', 'periodEnd', - 'period', 'range', 'budgetIncomeTotal', - 'defaultCurrency', 'inactive', 'budgets', - 'spent', 'budgeted' - ) + 'budgets.index', + compact('available', 'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets', 'spent', 'budgeted') ); } /** - * @param BudgetRepositoryInterface $repository + * @param Request $request * * @return View */ - public function noBudget(BudgetRepositoryInterface $repository) + public function noBudget(Request $request) { /** @var Carbon $start */ $start = session('start', Carbon::now()->startOfMonth()); /** @var Carbon $end */ - $end = session('end', Carbon::now()->endOfMonth()); - - $page = intval(Input::get('page')) == 0 ? 1 : intval(Input::get('page')); - $pageSize = Preferences::get('transactionPageSize', 50)->data; - $offset = ($page - 1) * $pageSize; - $journals = $repository->journalsInPeriodWithoutBudget(new Collection, $start, $end); // budget - $count = $journals->count(); - $journals = $journals->slice($offset, $pageSize); - $list = new LengthAwarePaginator($journals, $count, $pageSize); + $end = session('end', Carbon::now()->endOfMonth()); + $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $subTitle = trans( 'firefly.without_budget_between', ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] ); - $list->setPath('/budgets/list/noBudget'); - return view('budgets.noBudget', compact('list', 'subTitle')); + // collector + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withoutBudget(); + $journals = $collector->getPaginatedJournals(); + $journals->setPath('/budgets/list/noBudget'); + + return view('budgets.no-budget', compact('journals', 'subTitle')); } /** * @return \Illuminate\Http\RedirectResponse */ - public function postUpdateIncome() + public function postUpdateIncome(BudgetIncomeRequest $request) { - $range = Preferences::get('viewRange', '1M')->data; - /** @var Carbon $date */ - $date = session('start', new Carbon); - $start = Navigation::startOfPeriod($date, $range); - $end = Navigation::endOfPeriod($start, $range); - $key = 'budgetIncomeTotal' . $start->format('Ymd') . $end->format('Ymd'); + $start = session('start', new Carbon); + $end = session('end', new Carbon); + $defaultCurrency = Amount::getDefaultCurrency(); + $amount = $request->get('amount'); - Preferences::set($key, intval(Input::get('amount'))); + $this->repository->setAvailableBudget($defaultCurrency, $start, $end, $amount); Preferences::mark(); return redirect(route('budgets.index')); } /** - * @param BudgetRepositoryInterface $repository - * @param Budget $budget + * @param Request $request + * @param Budget $budget * * @return View - * @throws FireflyException */ - public function show(BudgetRepositoryInterface $repository, Budget $budget) + public function show(Request $request, Budget $budget) { /** @var Carbon $start */ - $start = session('first', Carbon::create()->startOfYear()); - $end = new Carbon; - $page = intval(Input::get('page')) == 0 ? 1 : intval(Input::get('page')); - $pageSize = Preferences::get('transactionPageSize', 50)->data; - $offset = ($page - 1) * $pageSize; - $journals = $repository->journalsInPeriod(new Collection([$budget]), new Collection, $start, $end); // budget - $count = $journals->count(); - $journals = $journals->slice($offset, $pageSize); - $journals = new LengthAwarePaginator($journals, $count, $pageSize); - + $start = session('first', Carbon::create()->startOfYear()); + $end = new Carbon; + $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; + // collector: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($start, $end)->setBudget($budget)->setLimit($pageSize)->setPage($page)->withCategoryInformation(); + $journals = $collector->getPaginatedJournals(); $journals->setPath('/budgets/show/' . $budget->id); - $set = $budget->limitrepetitions()->orderBy('startdate', 'DESC')->get(); $subTitle = e($budget->name); - $limits = new Collection(); - - /** @var LimitRepetition $entry */ - foreach ($set as $entry) { - $entry->spent = $repository->spentInPeriod(new Collection([$budget]), new Collection, $entry->startdate, $entry->enddate); - $limits->push($entry); - } return view('budgets.show', compact('limits', 'budget', 'repetition', 'journals', 'subTitle')); } /** - * @param BudgetRepositoryInterface $repository - * @param Budget $budget - * @param LimitRepetition $repetition + * @param Request $request + * @param Budget $budget + * @param BudgetLimit $budgetLimit * * @return View * @throws FireflyException */ - public function showWithRepetition(BudgetRepositoryInterface $repository, Budget $budget, LimitRepetition $repetition) + public function showByBudgetLimit(Request $request, Budget $budget, BudgetLimit $budgetLimit) { - if ($repetition->budgetLimit->budget->id != $budget->id) { + if ($budgetLimit->budget->id != $budget->id) { throw new FireflyException('This budget limit is not part of this budget.'); } - $start = $repetition->startdate; - $end = $repetition->enddate; - $page = intval(Input::get('page')) == 0 ? 1 : intval(Input::get('page')); - $pageSize = Preferences::get('transactionPageSize', 50)->data; - $offset = ($page - 1) * $pageSize; - $journals = $repository->journalsInPeriod(new Collection([$budget]), new Collection, $start, $end); // budget - $count = $journals->count(); - $journals = $journals->slice($offset, $pageSize); - $journals = new LengthAwarePaginator($journals, $count, $pageSize); - $subTitle = trans('firefly.budget_in_month', ['name' => $budget->name, 'month' => $repetition->startdate->formatLocalized($this->monthFormat)]); - $journals->setPath('/budgets/show/' . $budget->id . '/' . $repetition->id); + $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $subTitle = trans( + 'firefly.budget_in_period', [ + 'name' => $budget->name, + 'start' => $budgetLimit->start_date->formatLocalized($this->monthAndDayFormat), + 'end' => $budgetLimit->end_date->formatLocalized($this->monthAndDayFormat), + ] + ); + + // collector: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($budgetLimit->start_date, $budgetLimit->end_date) + ->setBudget($budget)->setLimit($pageSize)->setPage($page)->withCategoryInformation(); + $journals = $collector->getPaginatedJournals(); + $journals->setPath('/budgets/show/' . $budget->id . '/' . $budgetLimit->id); - $repetition->spent = $repository->spentInPeriod(new Collection([$budget]), new Collection, $repetition->startdate, $repetition->enddate); - $limits = new Collection([$repetition]); + $start = session('first', Carbon::create()->startOfYear()); + $end = new Carbon; + $limits = $this->getLimits($budget, $start, $end); - return view('budgets.show', compact('limits', 'budget', 'repetition', 'journals', 'subTitle')); + return view('budgets.show', compact('limits', 'budget', 'budgetLimit', 'journals', 'subTitle')); } /** - * @param BudgetFormRequest $request - * @param BudgetRepositoryInterface $repository + * @param BudgetFormRequest $request * * @return \Illuminate\Http\RedirectResponse */ - public function store(BudgetFormRequest $request, BudgetRepositoryInterface $repository) + public function store(BudgetFormRequest $request) { - $data = $request->getBudgetData(); - $budget = $repository->store($data); + $data = $request->getBudgetData(); + $budget = $this->repository->store($data); Session::flash('success', strval(trans('firefly.stored_new_budget', ['name' => e($budget->name)]))); Preferences::mark(); - if (intval(Input::get('create_another')) === 1) { + if (intval($request->get('create_another')) === 1) { // set value so create routine will not overwrite URL: Session::put('budgets.create.fromStore', true); return redirect(route('budgets.create'))->withInput(); } - // redirect to previous URL. - return redirect(session('budgets.create.url')); - + return redirect($this->getPreviousUri('budgets.create.uri')); } /** - * @param BudgetFormRequest $request - * @param BudgetRepositoryInterface $repository - * @param Budget $budget + * @param BudgetFormRequest $request + * @param Budget $budget * * @return \Illuminate\Http\RedirectResponse */ - public function update(BudgetFormRequest $request, BudgetRepositoryInterface $repository, Budget $budget) + public function update(BudgetFormRequest $request, Budget $budget) { $data = $request->getBudgetData(); - $repository->update($budget, $data); + $this->repository->update($budget, $data); Session::flash('success', strval(trans('firefly.updated_budget', ['name' => e($budget->name)]))); Preferences::mark(); - if (intval(Input::get('return_to_edit')) === 1) { + if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: Session::put('budgets.edit.fromUpdate', true); return redirect(route('budgets.edit', [$budget->id]))->withInput(['return_to_edit' => 1]); } - // redirect to previous URL. - return redirect(session('budgets.edit.url')); - + return redirect($this->getPreviousUri('budgets.edit.uri')); } /** @@ -409,19 +347,93 @@ class BudgetController extends Controller */ public function updateIncome() { - $range = Preferences::get('viewRange', '1M')->data; - $format = strval(trans('config.month_and_day')); + $start = session('start', new Carbon); + $end = session('end', new Carbon); + $defaultCurrency = Amount::getDefaultCurrency(); + $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end); - /** @var Carbon $date */ - $date = session('start', new Carbon); - $start = Navigation::startOfPeriod($date, $range); - $end = Navigation::endOfPeriod($start, $range); - $key = 'budgetIncomeTotal' . $start->format('Ymd') . $end->format('Ymd'); - $amount = Preferences::get($key, 1000); - $displayStart = $start->formatLocalized($format); - $displayEnd = $end->formatLocalized($format); - return view('budgets.income', compact('amount', 'displayStart', 'displayEnd')); + return view('budgets.income', compact('available', 'start', 'end')); + } + + /** + * @param Collection $budgets + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + private function collectBudgetInformation(Collection $budgets, Carbon $start, Carbon $end): array + { + // get account information + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH]); + $return = []; + /** @var Budget $budget */ + foreach ($budgets as $budget) { + $budgetId = $budget->id; + $return[$budgetId] = [ + 'spent' => $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end), + 'budgeted' => '0', + 'currentRep' => false, + ]; + $budgetLimits = $this->repository->getBudgetLimits($budget, $start, $end); + $otherLimits = new Collection; + + // get all the budget limits relevant between start and end and examine them: + /** @var BudgetLimit $limit */ + foreach ($budgetLimits as $limit) { + if ($limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end) + ) { + $return[$budgetId]['currentLimit'] = $limit; + $return[$budgetId]['budgeted'] = $limit->amount; + continue; + } + // otherwise it's just one of the many relevant repetitions: + $otherLimits->push($limit); + } + $return[$budgetId]['otherLimits'] = $otherLimits; + } + + return $return; + } + + + /** + * @param Budget $budget + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + private function getLimits(Budget $budget, Carbon $start, Carbon $end): Collection + { + // properties for cache + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($budget->id); + $cache->addProperty('get-limits'); + + if ($cache->has()) { + return $cache->get(); + } + + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH]); + $set = $this->repository->getBudgetLimits($budget, $start, $end); + $limits = new Collection(); + + /** @var BudgetLimit $entry */ + foreach ($set as $entry) { + $entry->spent = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $entry->start_date, $entry->end_date); + $limits->push($entry); + } + $cache->store($limits); + + return $set; } } diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index 342c581440..2224cb3512 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -14,19 +14,18 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers; use Carbon\Carbon; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Requests\CategoryFormRequest; use FireflyIII\Models\AccountType; use FireflyIII\Models\Category; use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Repositories\Category\CategoryRepositoryInterface as CRI; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Support\CacheProperties; -use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Http\Request; use Illuminate\Support\Collection; -use Input; use Navigation; use Preferences; use Session; -use URL; use View; /** @@ -43,8 +42,16 @@ class CategoryController extends Controller public function __construct() { parent::__construct(); - View::share('title', trans('firefly.categories')); - View::share('mainTitleIcon', 'fa-bar-chart'); + + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.categories')); + View::share('mainTitleIcon', 'fa-bar-chart'); + + return $next($request); + } + ); } /** @@ -52,9 +59,8 @@ class CategoryController extends Controller */ public function create() { - // put previous url in session if not redirect from store (not "create another"). if (session('categories.create.fromStore') !== true) { - Session::put('categories.create.url', URL::previous()); + $this->rememberPreviousUri('categories.create.uri'); } Session::forget('categories.create.fromStore'); Session::flash('gaEventCategory', 'categories'); @@ -74,20 +80,21 @@ class CategoryController extends Controller $subTitle = trans('firefly.delete_category', ['name' => $category->name]); // put previous url in session - Session::put('categories.delete.url', URL::previous()); + $this->rememberPreviousUri('categories.delete.uri'); Session::flash('gaEventCategory', 'categories'); Session::flash('gaEventAction', 'delete'); return view('categories.delete', compact('category', 'subTitle')); } + /** - * @param CRI $repository - * @param Category $category + * @param CategoryRepositoryInterface $repository + * @param Category $category * - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function destroy(CRI $repository, Category $category) + public function destroy(CategoryRepositoryInterface $repository, Category $category) { $name = $category->name; @@ -96,7 +103,7 @@ class CategoryController extends Controller Session::flash('success', strval(trans('firefly.deleted_category', ['name' => e($name)]))); Preferences::mark(); - return redirect(session('categories.delete.url')); + return redirect($this->getPreviousUri('categories.delete.uri')); } /** @@ -110,7 +117,7 @@ class CategoryController extends Controller // put previous url in session if not redirect from store (not "return_to_edit"). if (session('categories.edit.fromUpdate') !== true) { - Session::put('categories.edit.url', URL::previous()); + $this->rememberPreviousUri('categories.edit.uri'); } Session::forget('categories.edit.fromUpdate'); Session::flash('gaEventCategory', 'categories'); @@ -121,11 +128,11 @@ class CategoryController extends Controller } /** - * @param CRI $repository + * @param CategoryRepositoryInterface $repository * * @return View */ - public function index(CRI $repository) + public function index(CategoryRepositoryInterface $repository) { $categories = $repository->getCategories(); @@ -139,130 +146,125 @@ class CategoryController extends Controller } /** - * @param CRI $repository - * * @return View */ - public function noCategory(CRI $repository) + public function noCategory() { /** @var Carbon $start */ $start = session('start', Carbon::now()->startOfMonth()); /** @var Carbon $end */ - $end = session('end', Carbon::now()->startOfMonth()); - $list = $repository->journalsInPeriodWithoutCategory(new Collection(), [], $start, $end); // category + $end = session('end', Carbon::now()->startOfMonth()); + + // new collector: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($start, $end)->withoutCategory();//->groupJournals(); + $journals = $collector->getJournals(); $subTitle = trans( 'firefly.without_category_between', ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] ); - return view('categories.noCategory', compact('list', 'subTitle')); + return view('categories.no-category', compact('journals', 'subTitle')); } /** - * @param CRI $repository - * @param AccountRepositoryInterface $accountRepository - * @param Category $category + * @param Request $request + * @param JournalCollectorInterface $collector + * @param Category $category * * @return View */ - public function show(CRI $repository, AccountRepositoryInterface $accountRepository, Category $category) + public function show(Request $request, JournalCollectorInterface $collector, Category $category) { - $range = Preferences::get('viewRange', '1M')->data; - /** @var Carbon $start */ - $start = session('start', Navigation::startOfPeriod(new Carbon, $range)); - /** @var Carbon $end */ + $range = Preferences::get('viewRange', '1M')->data; + $start = session('start', Navigation::startOfPeriod(new Carbon, $range)); $end = session('end', Navigation::endOfPeriod(new Carbon, $range)); $hideCategory = true; // used in list. - $page = intval(Input::get('page')); - $pageSize = Preferences::get('transactionPageSize', 50)->data; - $offset = ($page - 1) * $pageSize; - $set = $repository->journalsInPeriod(new Collection([$category]), new Collection, [], $start, $end); // category - $count = $set->count(); - $subSet = $set->splice($offset, $pageSize); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $subTitle = $category->name; $subTitleIcon = 'fa-bar-chart'; - $journals = new LengthAwarePaginator($subSet, $count, $pageSize, $page); + $entries = $this->getGroupedEntries($category); + $method = 'default'; + + // get journals + $collector->setLimit($pageSize)->setPage($page)->setAllAssetAccounts()->setRange($start, $end)->setCategory($category)->withBudgetInformation(); + $journals = $collector->getPaginatedJournals(); $journals->setPath('categories/show/' . $category->id); - // oldest transaction in category: + + return view('categories.show', compact('category', 'method', 'journals', 'entries', 'hideCategory', 'subTitle', 'subTitleIcon', 'start', 'end')); + } + + /** + * @param Request $request + * @param CategoryRepositoryInterface $repository + * @param Category $category + * + * @return View + */ + public function showAll(Request $request, CategoryRepositoryInterface $repository, Category $category) + { + $range = Preferences::get('viewRange', '1M')->data; $start = $repository->firstUseDate($category); if ($start->year == 1900) { $start = new Carbon; } - $range = Preferences::get('viewRange', '1M')->data; - $start = Navigation::startOfPeriod($start, $range); - $end = Navigation::endOfX(new Carbon, $range); - $entries = new Collection; + $end = Navigation::endOfPeriod(new Carbon, $range); + $subTitle = $category->name; + $subTitleIcon = 'fa-bar-chart'; + $hideCategory = true; // used in list. + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $method = 'all'; - // chart properties for cache: - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('category-show'); - $cache->addProperty($category->id); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setLimit($pageSize)->setPage($page)->setAllAssetAccounts()->setCategory($category)->withBudgetInformation(); + $journals = $collector->getPaginatedJournals(); + $journals->setPath('categories/show/' . $category->id . '/all'); - - if ($cache->has()) { - $entries = $cache->get(); - - return view('categories.show', compact('category', 'journals', 'entries', 'subTitleIcon', 'hideCategory', 'subTitle')); - } - - - $categoryCollection = new Collection([$category]); - $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); - while ($end >= $start) { - $end = Navigation::startOfPeriod($end, $range); - $currentEnd = Navigation::endOfPeriod($end, $range); - $spent = $repository->spentInPeriod($categoryCollection, $accounts, $end, $currentEnd); - $earned = $repository->earnedInPeriod($categoryCollection, $accounts, $end, $currentEnd); - $dateStr = $end->format('Y-m-d'); - $dateName = Navigation::periodShow($end, $range); - $entries->push([$dateStr, $dateName, $spent, $earned]); - - $end = Navigation::subtractPeriod($end, $range, 1); - - } - $cache->store($entries); - - return view('categories.show', compact('category', 'journals', 'entries', 'hideCategory', 'subTitle')); + return view('categories.show', compact('category', 'method', 'journals', 'hideCategory', 'subTitle', 'subTitleIcon', 'start', 'end')); } /** - * @param CRI $repository - * @param Category $category - * - * @param $date + * @param Request $request + * @param Category $category + * @param string $date * * @return View */ - public function showWithDate(CRI $repository, Category $category, string $date) + public function showByDate(Request $request, Category $category, string $date) { $carbon = new Carbon($date); $range = Preferences::get('viewRange', '1M')->data; $start = Navigation::startOfPeriod($carbon, $range); $end = Navigation::endOfPeriod($carbon, $range); $subTitle = $category->name; + $subTitleIcon = 'fa-bar-chart'; $hideCategory = true; // used in list. - $page = intval(Input::get('page')); - $pageSize = Preferences::get('transactionPageSize', 50)->data; - $offset = ($page - 1) * $pageSize; - $set = $repository->journalsInPeriod(new Collection([$category]), new Collection, [], $start, $end); // category - $count = $set->count(); - $subSet = $set->splice($offset, $pageSize); - $journals = new LengthAwarePaginator($subSet, $count, $pageSize, $page); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $entries = $this->getGroupedEntries($category); + $method = 'date'; + + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setLimit($pageSize)->setPage($page)->setAllAssetAccounts()->setRange($start, $end)->setCategory($category)->withBudgetInformation(); + $journals = $collector->getPaginatedJournals(); $journals->setPath('categories/show/' . $category->id . '/' . $date); - return view('categories.show_with_date', compact('category', 'journals', 'hideCategory', 'subTitle', 'carbon')); + return view('categories.show', compact('category', 'method', 'entries', 'journals', 'hideCategory', 'subTitle', 'subTitleIcon', 'start', 'end')); } /** - * @param CategoryFormRequest $request - * @param CRI $repository + * @param CategoryFormRequest $request + * @param CategoryRepositoryInterface $repository * - * @return \Illuminate\Http\RedirectResponse + * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function store(CategoryFormRequest $request, CRI $repository) + public function store(CategoryFormRequest $request, CategoryRepositoryInterface $repository) { $data = $request->getCategoryData(); $category = $repository->store($data); @@ -270,7 +272,7 @@ class CategoryController extends Controller Session::flash('success', strval(trans('firefly.stored_category', ['name' => e($category->name)]))); Preferences::mark(); - if (intval(Input::get('create_another')) === 1) { + if (intval($request->get('create_another')) === 1) { Session::put('categories.create.fromStore', true); return redirect(route('categories.create'))->withInput(); @@ -281,13 +283,13 @@ class CategoryController extends Controller /** - * @param CategoryFormRequest $request - * @param CRI $repository - * @param Category $category + * @param CategoryFormRequest $request + * @param CategoryRepositoryInterface $repository + * @param Category $category * - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function update(CategoryFormRequest $request, CRI $repository, Category $category) + public function update(CategoryFormRequest $request, CategoryRepositoryInterface $repository, Category $category) { $data = $request->getCategoryData(); $repository->update($category, $data); @@ -295,15 +297,59 @@ class CategoryController extends Controller Session::flash('success', strval(trans('firefly.updated_category', ['name' => e($category->name)]))); Preferences::mark(); - if (intval(Input::get('return_to_edit')) === 1) { + if (intval($request->get('return_to_edit')) === 1) { Session::put('categories.edit.fromUpdate', true); return redirect(route('categories.edit', [$category->id])); } - // redirect to previous URL. - return redirect(session('categories.edit.url')); + return redirect($this->getPreviousUri('categories.edit.uri')); + } + /** + * @param Category $category + * + * @return Collection + */ + private function getGroupedEntries(Category $category): Collection + { + /** @var CategoryRepositoryInterface $repository */ + $repository = app(CategoryRepositoryInterface::class); + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + $first = $repository->firstUseDate($category); + if ($first->year == 1900) { + $first = new Carbon; + } + $range = Preferences::get('viewRange', '1M')->data; + $first = Navigation::startOfPeriod($first, $range); + $end = Navigation::endOfX(new Carbon, $range); + $entries = new Collection; + + // properties for entries with their amounts. + $cache = new CacheProperties(); + $cache->addProperty($first); + $cache->addProperty($end); + $cache->addProperty('categories.entries'); + $cache->addProperty($category->id); + + if ($cache->has()) { + return $cache->get(); + } + while ($end >= $first) { + $end = Navigation::startOfPeriod($end, $range); + $currentEnd = Navigation::endOfPeriod($end, $range); + $spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $end, $currentEnd); + $earned = $repository->earnedInPeriod(new Collection([$category]), $accounts, $end, $currentEnd); + $dateStr = $end->format('Y-m-d'); + $dateName = Navigation::periodShow($end, $range); + $entries->push([$dateStr, $dateName, $spent, $earned, clone $end]); + $end = Navigation::subtractPeriod($end, $range, 1); + } + $cache->store($entries); + + return $entries; } } diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index ade478c200..5391d06883 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -16,11 +16,16 @@ namespace FireflyIII\Http\Controllers\Chart; use Carbon\Carbon; use Exception; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Generator\Chart\Account\AccountChartGeneratorInterface; +use FireflyIII\Generator\Chart\Basic\GeneratorInterface; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; use Log; @@ -37,7 +42,7 @@ use Steam; class AccountController extends Controller { - /** @var \FireflyIII\Generator\Chart\Account\AccountChartGeneratorInterface */ + /** @var GeneratorInterface */ protected $generator; /** @@ -46,8 +51,49 @@ class AccountController extends Controller public function __construct() { parent::__construct(); - // create chart generator: - $this->generator = app(AccountChartGeneratorInterface::class); + $this->generator = app(GeneratorInterface::class); + } + + /** + * @param Account $account + * + * @return \Illuminate\Http\JsonResponse + */ + public function all(Account $account) + { + $cache = new CacheProperties(); + $cache->addProperty('chart.account.all'); + $cache->addProperty($account->id); + if ($cache->has()) { + Log::debug('Return chart.account.all from cache.'); + + return Response::json($cache->get()); + } + Log::debug('Regenerate chart.account.all from scratch.'); + + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $start = $repository->oldestJournalDate($account); + $end = new Carbon; + $format = (string)trans('config.month_and_day'); + $range = Steam::balanceInRange($account, $start, $end); + $current = clone $start; + $previous = array_values($range)[0]; + $chartData = []; + + while ($end >= $current) { + $theDate = $current->format('Y-m-d'); + $balance = $range[$theDate] ?? $previous; + $label = $current->formatLocalized($format); + $chartData[$label] = $balance; + $previous = $balance; + $current->addDay(); + } + + $data = $this->generator->singleSet($account->name, $chartData); + $cache->store($data); + + return Response::json($data); } /** @@ -64,41 +110,124 @@ class AccountController extends Controller $cache = new CacheProperties; $cache->addProperty($start); $cache->addProperty($end); - $cache->addProperty('expenseAccounts'); - $cache->addProperty('accounts'); + $cache->addProperty('chart.account.expense-accounts'); if ($cache->has()) { return Response::json($cache->get()); } - $accounts = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]); - $start->subDay(); + + $accounts = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]); $ids = $accounts->pluck('id')->toArray(); $startBalances = Steam::balancesById($ids, $start); $endBalances = Steam::balancesById($ids, $end); + $chartData = []; - $accounts->each( - function (Account $account) use ($startBalances, $endBalances) { - $id = $account->id; - $startBalance = $startBalances[$id] ?? '0'; - $endBalance = $endBalances[$id] ?? '0'; - $diff = bcsub($endBalance, $startBalance); - $account->difference = round($diff, 2); + foreach ($accounts as $account) { + $id = $account->id; + $startBalance = $startBalances[$id] ?? '0'; + $endBalance = $endBalances[$id] ?? '0'; + $diff = bcsub($endBalance, $startBalance); + if (bccomp($diff, '0') !== 0) { + $chartData[$account->name] = $diff; } - ); - - - $accounts = $accounts->sortByDesc( - function (Account $account) { - return $account->difference; - } - ); - - $data = $this->generator->expenseAccounts($accounts, $start, $end); + } + arsort($chartData); + $data = $this->generator->singleSet(strval(trans('firefly.spent')), $chartData); $cache->store($data); return Response::json($data); } + /** + * @param JournalCollectorInterface $collector + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function expenseBudget(JournalCollectorInterface $collector, Account $account, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty($account->id); + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('chart.account.expense-budget'); + if ($cache->has()) { + return Response::json($cache->get()); + } + $collector->setAccounts(new Collection([$account])) + ->setRange($start, $end) + ->withBudgetInformation() + ->setTypes([TransactionType::WITHDRAWAL]); + $transactions = $collector->getJournals(); + $chartData = []; + $result = []; + + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $jrnlBudgetId = intval($transaction->transaction_journal_budget_id); + $transBudgetId = intval($transaction->transaction_budget_id); + $budgetId = max($jrnlBudgetId, $transBudgetId); + $result[$budgetId] = $result[$budgetId] ?? '0'; + $result[$budgetId] = bcadd($transaction->transaction_amount, $result[$budgetId]); + } + + $names = $this->getBudgetNames(array_keys($result)); + foreach ($result as $budgetId => $amount) { + $chartData[$names[$budgetId]] = $amount; + } + + $data = $this->generator->pieChart($chartData); + $cache->store($data); + + return Response::json($data); + } + + /** + * @param JournalCollectorInterface $collector + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function expenseCategory(JournalCollectorInterface $collector, Account $account, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty($account->id); + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('chart.account.expense-category'); + if ($cache->has()) { + return Response::json($cache->get()); + } + + $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->withCategoryInformation()->setTypes([TransactionType::WITHDRAWAL]); + $transactions = $collector->getJournals(); + $result = []; + $chartData = []; + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $jrnlCatId = intval($transaction->transaction_journal_category_id); + $transCatId = intval($transaction->transaction_category_id); + $categoryId = max($jrnlCatId, $transCatId); + $result[$categoryId] = $result[$categoryId] ?? '0'; + $result[$categoryId] = bcadd($transaction->transaction_amount, $result[$categoryId]); + } + + $names = $this->getCategoryNames(array_keys($result)); + foreach ($result as $categoryId => $amount) { + $chartData[$names[$categoryId]] = $amount; + } + + $data = $this->generator->pieChart($chartData); + $cache->store($data); + + return Response::json($data); + + } + /** * Shows the balances for all the user's frontpage accounts. * @@ -108,38 +237,108 @@ class AccountController extends Controller */ public function frontpage(AccountRepositoryInterface $repository) { - $start = clone session('start', Carbon::now()->startOfMonth()); - $end = clone session('end', Carbon::now()->endOfMonth()); + $start = clone session('start', Carbon::now()->startOfMonth()); + $end = clone session('end', Carbon::now()->endOfMonth()); + $defaultSet = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])->pluck('id')->toArray(); + Log::debug('Default set is ', $defaultSet); + $frontPage = Preferences::get('frontPageAccounts', $defaultSet); + Log::debug('Frontpage preference set is ', $frontPage->data); + if (count($frontPage->data) === 0) { + $frontPage->data = $defaultSet; + Log::debug('frontpage set is empty!'); + $frontPage->save(); + } + $accounts = $repository->getAccountsById($frontPage->data); + return Response::json($this->accountBalanceChart($accounts, $start, $end)); + } - // chart properties for cache: + /** + * @param JournalCollectorInterface $collector + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function incomeCategory(JournalCollectorInterface $collector, Account $account, Carbon $start, Carbon $end) + { $cache = new CacheProperties; + $cache->addProperty($account->id); $cache->addProperty($start); $cache->addProperty($end); - $cache->addProperty('frontpage'); - $cache->addProperty('accounts'); + $cache->addProperty('chart.account.income-category'); if ($cache->has()) { return Response::json($cache->get()); } - $frontPage = Preferences::get('frontPageAccounts', $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])->pluck('id')->toArray()); - $accounts = $repository->getAccountsById($frontPage->data); - - foreach ($accounts as $account) { - $balances = []; - $current = clone $start; - $range = Steam::balanceInRange($account, $start, clone $end); - $previous = round(array_values($range)[0], 2); - while ($current <= $end) { - $format = $current->format('Y-m-d'); - $balance = isset($range[$format]) ? round($range[$format], 2) : $previous; - $previous = $balance; - $balances[] = $balance; - $current->addDay(); - } - $account->balances = $balances; + // grab all journals: + $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->withCategoryInformation()->setTypes([TransactionType::DEPOSIT]); + $transactions = $collector->getJournals(); + $result = []; + $chartData = []; + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $jrnlCatId = intval($transaction->transaction_journal_category_id); + $transCatId = intval($transaction->transaction_category_id); + $categoryId = max($jrnlCatId, $transCatId); + $result[$categoryId] = $result[$categoryId] ?? '0'; + $result[$categoryId] = bcadd($transaction->transaction_amount, $result[$categoryId]); } - $data = $this->generator->frontpage($accounts, $start, $end); + + $names = $this->getCategoryNames(array_keys($result)); + foreach ($result as $categoryId => $amount) { + $chartData[$names[$categoryId]] = $amount; + } + $data = $this->generator->pieChart($chartData); + $cache->store($data); + + return Response::json($data); + + } + + /** + * @param Account $account + * @param string $date + * + * @return \Illuminate\Http\JsonResponse + * @throws FireflyException + */ + public function period(Account $account, string $date) + { + try { + $start = new Carbon($date); + } catch (Exception $e) { + Log::error($e->getMessage()); + throw new FireflyException('"' . e($date) . '" does not seem to be a valid date. Should be in the format YYYY-MM-DD'); + } + $range = Preferences::get('viewRange', '1M')->data; + $end = Navigation::endOfPeriod($start, $range); + $cache = new CacheProperties(); + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('chart.account.period'); + $cache->addProperty($account->id); + if ($cache->has()) { + return Response::json($cache->get()); + } + + $format = (string)trans('config.month_and_day'); + $range = Steam::balanceInRange($account, $start, $end); + $current = clone $start; + $previous = array_values($range)[0]; + $chartData = []; + + while ($end >= $current) { + $theDate = $current->format('Y-m-d'); + $balance = $range[$theDate] ?? $previous; + $label = $current->formatLocalized($format); + $chartData[$label] = $balance; + $previous = $balance; + $current->addDay(); + } + + $data = $this->generator->singleSet($account->name, $chartData); $cache->store($data); return Response::json($data); @@ -154,40 +353,9 @@ class AccountController extends Controller * * @return \Illuminate\Http\JsonResponse */ - public function report(Carbon $start, Carbon $end, Collection $accounts) + public function report(Collection $accounts, Carbon $start, Carbon $end) { - // chart properties for cache: - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('all'); - $cache->addProperty('accounts'); - $cache->addProperty('default'); - $cache->addProperty($accounts); - if ($cache->has()) { - return Response::json($cache->get()); - } - - foreach ($accounts as $account) { - $balances = []; - $current = clone $start; - $range = Steam::balanceInRange($account, $start, clone $end); - $previous = round(array_values($range)[0], 2); - while ($current <= $end) { - $format = $current->format('Y-m-d'); - $balance = isset($range[$format]) ? round($range[$format], 2) : $previous; - $previous = $balance; - $balances[] = $balance; - $current->addDay(); - } - $account->balances = $balances; - } - - // make chart: - $data = $this->generator->frontpage($accounts, $start, $end); - $cache->store($data); - - return Response::json($data); + return Response::json($this->accountBalanceChart($accounts, $start, $end)); } /** @@ -199,13 +367,13 @@ class AccountController extends Controller */ public function revenueAccounts(AccountRepositoryInterface $repository) { - $start = clone session('start', Carbon::now()->startOfMonth()); - $end = clone session('end', Carbon::now()->endOfMonth()); - $cache = new CacheProperties; + $start = clone session('start', Carbon::now()->startOfMonth()); + $end = clone session('end', Carbon::now()->endOfMonth()); + $chartData = []; + $cache = new CacheProperties; $cache->addProperty($start); $cache->addProperty($end); - $cache->addProperty('revenueAccounts'); - $cache->addProperty('accounts'); + $cache->addProperty('chart.account.revenue-accounts'); if ($cache->has()) { return Response::json($cache->get()); } @@ -216,25 +384,19 @@ class AccountController extends Controller $startBalances = Steam::balancesById($ids, $start); $endBalances = Steam::balancesById($ids, $end); - $accounts->each( - function (Account $account) use ($startBalances, $endBalances) { - $id = $account->id; - $startBalance = $startBalances[$id] ?? '0'; - $endBalance = $endBalances[$id] ?? '0'; - $diff = bcsub($endBalance, $startBalance); - $diff = bcmul($diff, '-1'); - $account->difference = round($diff, 2); + foreach ($accounts as $account) { + $id = $account->id; + $startBalance = $startBalances[$id] ?? '0'; + $endBalance = $endBalances[$id] ?? '0'; + $diff = bcsub($endBalance, $startBalance); + $diff = bcmul($diff, '-1'); + if (bccomp($diff, '0') !== 0) { + $chartData[$account->name] = $diff; } - ); + } - - $accounts = $accounts->sortByDesc( - function (Account $account) { - return $account->difference; - } - ); - - $data = $this->generator->revenueAccounts($accounts, $start, $end); + arsort($chartData); + $data = $this->generator->singleSet(strval(trans('firefly.spent')), $chartData); $cache->store($data); return Response::json($data); @@ -256,8 +418,7 @@ class AccountController extends Controller $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); - $cache->addProperty('frontpage'); - $cache->addProperty('single'); + $cache->addProperty('chart.account.single'); $cache->addProperty($account->id); if ($cache->has()) { return Response::json($cache->get()); @@ -267,77 +428,115 @@ class AccountController extends Controller $range = Steam::balanceInRange($account, $start, $end); $current = clone $start; $previous = array_values($range)[0]; - $labels = []; $chartData = []; while ($end >= $current) { - $theDate = $current->format('Y-m-d'); - $balance = $range[$theDate] ?? $previous; - - $labels[] = $current->formatLocalized($format); - $chartData[] = $balance; - $previous = $balance; + $theDate = $current->format('Y-m-d'); + $balance = $range[$theDate] ?? $previous; + $label = $current->formatLocalized($format); + $chartData[$label] = $balance; + $previous = $balance; $current->addDay(); } - - $data = $this->generator->single($account, $labels, $chartData); + $data = $this->generator->singleSet($account->name, $chartData); $cache->store($data); return Response::json($data); } - /** - * @param Account $account - * @param string $date + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end * - * @return \Illuminate\Http\JsonResponse - * @throws FireflyException + * @return array */ - public function specificPeriod(Account $account, string $date) + private function accountBalanceChart(Collection $accounts, Carbon $start, Carbon $end): array { - try { - $start = new Carbon($date); - } catch (Exception $e) { - Log::error($e->getMessage()); - throw new FireflyException('"' . e($date) . '" does not seem to be a valid date. Should be in the format YYYY-MM-DD'); - } - $range = Preferences::get('viewRange', '1M')->data; - $end = Navigation::endOfPeriod($start, $range); // chart properties for cache: $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); - $cache->addProperty('frontpage'); - $cache->addProperty('specificPeriod'); - $cache->addProperty($account->id); + $cache->addProperty('chart.account.account-balance-chart'); + $cache->addProperty($accounts); if ($cache->has()) { - return Response::json($cache->get()); - } + Log::debug('Return chart.account.account-balance-chart from cache.'); + + return $cache->get(); + } + Log::debug('Regenerate chart.account.account-balance-chart from scratch.'); - $format = (string)trans('config.month_and_day'); - $range = Steam::balanceInRange($account, $start, $end); - $current = clone $start; - $previous = array_values($range)[0]; - $labels = []; $chartData = []; - - while ($end >= $current) { - $theDate = $current->format('Y-m-d'); - $balance = $range[$theDate] ?? $previous; - - $labels[] = $current->formatLocalized($format); - $chartData[] = $balance; - $previous = $balance; - $current->addDay(); + foreach ($accounts as $account) { + $currentSet = [ + 'label' => $account->name, + 'entries' => [], + ]; + $currentStart = clone $start; + $range = Steam::balanceInRange($account, $start, clone $end); + $previous = array_values($range)[0]; + while ($currentStart <= $end) { + $format = $currentStart->format('Y-m-d'); + $label = $currentStart->formatLocalized(strval(trans('config.month_and_day'))); + $balance = isset($range[$format]) ? round($range[$format], 12) : $previous; + $previous = $balance; + $currentStart->addDay(); + $currentSet['entries'][$label] = $balance; + } + $chartData[] = $currentSet; } - - - $data = $this->generator->single($account, $labels, $chartData); + $data = $this->generator->multiSet($chartData); $cache->store($data); - return Response::json($data); + return $data; + } + + /** + * @param array $budgetIds + * + * @return array + */ + private function getBudgetNames(array $budgetIds): array + { + + /** @var BudgetRepositoryInterface $repository */ + $repository = app(BudgetRepositoryInterface::class); + $budgets = $repository->getBudgets(); + $grouped = $budgets->groupBy('id')->toArray(); + $return = []; + foreach ($budgetIds as $budgetId) { + if (isset($grouped[$budgetId])) { + $return[$budgetId] = $grouped[$budgetId][0]['name']; + } + } + $return[0] = trans('firefly.no_budget'); + + return $return; + } + + /** + * Small helper function for some of the charts. + * + * @param array $categoryIds + * + * @return array + */ + private function getCategoryNames(array $categoryIds): array + { + /** @var CategoryRepositoryInterface $repository */ + $repository = app(CategoryRepositoryInterface::class); + $categories = $repository->getCategories(); + $grouped = $categories->groupBy('id')->toArray(); + $return = []; + foreach ($categoryIds as $categoryId) { + if (isset($grouped[$categoryId])) { + $return[$categoryId] = $grouped[$categoryId][0]['name']; + } + } + $return[0] = trans('firefly.noCategory'); + + return $return; } } diff --git a/app/Http/Controllers/Chart/BillController.php b/app/Http/Controllers/Chart/BillController.php index 78385b915b..14588e0720 100644 --- a/app/Http/Controllers/Chart/BillController.php +++ b/app/Http/Controllers/Chart/BillController.php @@ -14,12 +14,14 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers\Chart; use Carbon\Carbon; -use FireflyIII\Generator\Chart\Bill\BillChartGeneratorInterface; +use FireflyIII\Generator\Chart\Basic\GeneratorInterface; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Bill; -use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\Transaction; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Support\CacheProperties; +use Illuminate\Support\Collection; use Response; /** @@ -30,7 +32,7 @@ use Response; class BillController extends Controller { - /** @var \FireflyIII\Generator\Chart\Bill\BillChartGeneratorInterface */ + /** @var GeneratorInterface */ protected $generator; /** @@ -39,8 +41,7 @@ class BillController extends Controller public function __construct() { parent::__construct(); - // create chart generator: - $this->generator = app(BillChartGeneratorInterface::class); + $this->generator = app(GeneratorInterface::class); } /** @@ -52,44 +53,65 @@ class BillController extends Controller */ public function frontpage(BillRepositoryInterface $repository) { - $start = session('start', Carbon::now()->startOfMonth()); - $end = session('end', Carbon::now()->endOfMonth()); - $paid = $repository->getBillsPaidInRange($start, $end); // will be a negative amount. - $unpaid = $repository->getBillsUnpaidInRange($start, $end); // will be a positive amount. - $data = $this->generator->frontpage($paid, $unpaid); + $start = session('start', Carbon::now()->startOfMonth()); + $end = session('end', Carbon::now()->endOfMonth()); + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('chart.bill.frontpage'); + if ($cache->has()) { + return Response::json($cache->get()); + } + + $paid = $repository->getBillsPaidInRange($start, $end); // will be a negative amount. + $unpaid = $repository->getBillsUnpaidInRange($start, $end); // will be a positive amount. + $chartData = [ + strval(trans('firefly.unpaid')) => $unpaid, + strval(trans('firefly.paid')) => $paid, + ]; + + $data = $this->generator->pieChart($chartData); + $cache->store($data); return Response::json($data); } /** - * Shows the overview for a bill. The min/max amount and matched journals. + * @param JournalCollectorInterface $collector + * @param Bill $bill * - * @param BillRepositoryInterface $repository - * @param Bill $bill - * - * @return \Symfony\Component\HttpFoundation\Response + * @return \Illuminate\Http\JsonResponse */ - public function single(BillRepositoryInterface $repository, Bill $bill) + public function single(JournalCollectorInterface $collector, Bill $bill) { $cache = new CacheProperties; - $cache->addProperty('single'); - $cache->addProperty('bill'); + $cache->addProperty('chart.bill.single'); $cache->addProperty($bill->id); if ($cache->has()) { return Response::json($cache->get()); } - // get first transaction or today for start: - $results = $repository->getJournals($bill, 1, 200); - - // resort: - $results = $results->sortBy( - function (TransactionJournal $journal) { - return $journal->date->format('U'); + $results = $collector->setAllAssetAccounts()->setBills(new Collection([$bill]))->getJournals(); + $results = $results->sortBy( + function (Transaction $transaction) { + return $transaction->date->format('U'); } ); + $chartData = [ + ['type' => 'bar', 'label' => trans('firefly.min-amount'), 'entries' => [],], + ['type' => 'bar', 'label' => trans('firefly.max-amount'), 'entries' => [],], + ['type' => 'line', 'label' => trans('firefly.journal-amount'), 'entries' => [],], + ]; - $data = $this->generator->single($bill, $results); + /** @var Transaction $entry */ + foreach ($results as $entry) { + $date = $entry->date->formatLocalized(strval(trans('config.month_and_day'))); + $chartData[0]['entries'][$date] = $bill->amount_min; // minimum amount of bill + $chartData[1]['entries'][$date] = $bill->amount_max; // maximum amount of bill + $chartData[2]['entries'][$date] = bcmul($entry->transaction_amount, '-1'); // amount of journal + } + + $data = $this->generator->multiSet($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 56eeb966ef..cf99421aee 100644 --- a/app/Http/Controllers/Chart/BudgetController.php +++ b/app/Http/Controllers/Chart/BudgetController.php @@ -14,15 +14,17 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers\Chart; use Carbon\Carbon; -use FireflyIII\Generator\Chart\Budget\BudgetChartGeneratorInterface; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Generator\Chart\Basic\GeneratorInterface; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Budget; -use FireflyIII\Models\LimitRepetition; -use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; -use Log; use Navigation; use Preferences; use Response; @@ -30,42 +32,52 @@ use Response; /** * Class BudgetController * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) // can't realy be helped. + * * @package FireflyIII\Http\Controllers\Chart */ class BudgetController extends Controller { - /** @var BudgetChartGeneratorInterface */ + /** @var GeneratorInterface */ protected $generator; + /** @var BudgetRepositoryInterface */ + protected $repository; + /** * BudgetController constructor. */ public function __construct() { parent::__construct(); - // create chart generator: - $this->generator = app(BudgetChartGeneratorInterface::class); + + $this->middleware( + function ($request, $next) { + $this->generator = app(GeneratorInterface::class); + $this->repository = app(BudgetRepositoryInterface::class); + + return $next($request); + } + ); } /** - * checked * - * @param BudgetRepositoryInterface $repository - * @param Budget $budget + * @param Budget $budget * * @return \Symfony\Component\HttpFoundation\Response */ - public function budget(BudgetRepositoryInterface $repository, Budget $budget) + public function budget(Budget $budget) { - $first = $repository->firstUseDate($budget); + $first = $this->repository->firstUseDate($budget); $range = Preferences::get('viewRange', '1M')->data; $last = session('end', new Carbon); $cache = new CacheProperties(); $cache->addProperty($first); $cache->addProperty($last); - $cache->addProperty('budget'); + $cache->addProperty('chart.budget.budget'); if ($cache->has()) { return Response::json($cache->get()); @@ -73,10 +85,9 @@ class BudgetController extends Controller $final = clone $last; $final->addYears(2); - $budgetCollection = new Collection([$budget]); $last = Navigation::endOfX($last, $range, $final); // not to overshoot. - $entries = new Collection; + $entries = []; while ($first < $last) { // periodspecific dates: @@ -84,13 +95,14 @@ class BudgetController extends Controller $currentEnd = Navigation::endOfPeriod($first, $range); // sub another day because reasons. $currentEnd->subDay(); - $spent = $repository->spentInPeriod($budgetCollection, new Collection, $currentStart, $currentEnd); - $entry = [$first, ($spent * -1)]; - $entries->push($entry); - $first = Navigation::addPeriod($first, $range, 0); + $spent = $this->repository->spentInPeriod($budgetCollection, new Collection, $currentStart, $currentEnd); + $format = Navigation::periodShow($first, $range); + $entries[$format] = bcmul($spent, '-1'); + $first = Navigation::addPeriod($first, $range, 0); } - $data = $this->generator->budgetLimit($entries, 'month'); + $data = $this->generator->singleSet(strval(trans('firefly.spent')), $entries); + $cache->store($data); return Response::json($data); @@ -99,38 +111,43 @@ class BudgetController extends Controller /** * Shows the amount left in a specific budget limit. * - * @param BudgetRepositoryInterface $repository - * @param Budget $budget - * @param LimitRepetition $repetition + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. + * @param Budget $budget + * @param BudgetLimit $budgetLimit * * @return \Symfony\Component\HttpFoundation\Response + * @throws FireflyException */ - public function budgetLimit(BudgetRepositoryInterface $repository, Budget $budget, LimitRepetition $repetition) + public function budgetLimit(Budget $budget, BudgetLimit $budgetLimit) { - $start = clone $repetition->startdate; - $end = $repetition->enddate; + if ($budgetLimit->budget->id != $budget->id) { + throw new FireflyException('This budget limit is not part of this budget.'); + } + + $start = clone $budgetLimit->start_date; + $end = clone $budgetLimit->end_date; $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); - $cache->addProperty('budget-limit'); - $cache->addProperty($budget->id); - $cache->addProperty($repetition->id); + $cache->addProperty('chart.budget.budget.limit'); + $cache->addProperty($budgetLimit->id); if ($cache->has()) { return Response::json($cache->get()); } - $entries = new Collection; - $amount = $repetition->amount; + $entries = []; + $amount = $budgetLimit->amount; $budgetCollection = new Collection([$budget]); while ($start <= $end) { - $spent = $repository->spentInPeriod($budgetCollection, new Collection, $start, $start); - $amount = bcadd($amount, $spent); - $entries->push([clone $start, round($amount, 2)]); + $spent = $this->repository->spentInPeriod($budgetCollection, new Collection, $start, $start); + $amount = bcadd($amount, $spent); + $format = $start->formatLocalized(strval(trans('config.month_and_day'))); + $entries[$format] = $amount; $start->addDay(); } - $data = $this->generator->budgetLimit($entries, 'month_and_day'); + $data = $this->generator->singleSet(strval(trans('firefly.left')), $entries); $cache->store($data); return Response::json($data); @@ -138,135 +155,69 @@ class BudgetController extends Controller /** * Shows a budget list with spent/left/overspent. - * - * @param BudgetRepositoryInterface $repository + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) // 46 lines, I'm fine with this. * * @return \Symfony\Component\HttpFoundation\Response */ - public function frontpage(BudgetRepositoryInterface $repository) + public function frontpage() { - Log::debug('Hello'); $start = session('start', Carbon::now()->startOfMonth()); $end = session('end', Carbon::now()->endOfMonth()); // chart properties for cache: $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); - $cache->addProperty('budget'); - $cache->addProperty('all'); + $cache->addProperty('chart.budget.frontpage'); if ($cache->has()) { return Response::json($cache->get()); } - $budgets = $repository->getActiveBudgets(); - $repetitions = $repository->getAllBudgetLimitRepetitions($start, $end); - $allEntries = new Collection; + $budgets = $this->repository->getActiveBudgets(); + $chartData = [ + ['label' => strval(trans('firefly.spent_in_budget')), 'entries' => [], 'type' => 'bar',], + ['label' => strval(trans('firefly.left_to_spend')), 'entries' => [], 'type' => 'bar',], + ['label' => strval(trans('firefly.overspent')), 'entries' => [], 'type' => 'bar',], + ]; + /** @var Budget $budget */ foreach ($budgets as $budget) { // get relevant repetitions: - $reps = $this->filterRepetitions($repetitions, $budget, $start, $end); - - if ($reps->count() === 0) { - $collection = $this->spentInPeriodSingle($repository, $budget, $start, $end); - $allEntries = $allEntries->merge($collection); - continue; + $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']; + $chartData[2]['entries'][$name] = $row['overspent']; } - $collection = $this->spentInPeriodMulti($repository, $budget, $reps); - $allEntries = $allEntries->merge($collection); - } - $entry = $this->spentInPeriodWithout($repository, $start, $end); - $allEntries->push($entry); - $data = $this->generator->frontpage($allEntries); + // for no budget: + $spent = $this->spentInPeriodWithout($start, $end); + $name = strval(trans('firefly.no_budget')); + if (bccomp($spent, '0') !== 0) { + $chartData[0]['entries'][$name] = bcmul($spent, '-1'); + $chartData[1]['entries'][$name] = '0'; + $chartData[2]['entries'][$name] = '0'; + } + + $data = $this->generator->multiSet($chartData); $cache->store($data); return Response::json($data); } + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. * - * @param BudgetRepositoryInterface $repository - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * @param Collection $budgets - * + * @param Budget $budget + * @param Carbon $start + * @param Carbon $end + * @param Collection $accounts * * @return \Illuminate\Http\JsonResponse */ - public function multiYear(BudgetRepositoryInterface $repository, Carbon $start, Carbon $end, Collection $accounts, Collection $budgets) - { - - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($accounts); - $cache->addProperty($budgets); - $cache->addProperty('multiYearBudget'); - - if ($cache->has()) { - return Response::json($cache->get()); - } - $budgetIds = $budgets->pluck('id')->toArray(); - $repetitions = $repository->getAllBudgetLimitRepetitions($start, $end); - $budgeted = []; - $entries = new Collection; - // filter budgets once: - $repetitions = $repetitions->filter( - function (LimitRepetition $repetition) use ($budgetIds) { - if (in_array(strval($repetition->budget_id), $budgetIds)) { - return true; - } - - return false; - } - ); - /** @var LimitRepetition $repetition */ - foreach ($repetitions as $repetition) { - $year = $repetition->startdate->year; - if (isset($budgeted[$repetition->budget_id][$year])) { - $budgeted[$repetition->budget_id][$year] = bcadd($budgeted[$repetition->budget_id][$year], $repetition->amount); - continue; - } - $budgeted[$repetition->budget_id][$year] = $repetition->amount; - } - - foreach ($budgets as $budget) { - $currentStart = clone $start; - $entry = ['name' => $budget->name, 'spent' => [], 'budgeted' => []]; - while ($currentStart < $end) { - // fix the date: - $currentEnd = clone $currentStart; - $year = $currentStart->year; - $currentEnd->endOfYear(); - - $spent = $repository->spentInPeriod(new Collection([$budget]), $accounts, $currentStart, $currentEnd); - - // jump to next year. - $currentStart = clone $currentEnd; - $currentStart->addDay(); - - $entry['spent'][$year] = round($spent * -1, 2); - $entry['budgeted'][$year] = isset($budgeted[$budget->id][$year]) ? round($budgeted[$budget->id][$year], 2) : 0; - } - $entries->push($entry); - } - $data = $this->generator->multiYear($entries); - $cache->store($data); - - return Response::json($data); - } - - /** - * @param BudgetRepositoryInterface $repository - * @param Budget $budget - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return \Illuminate\Http\JsonResponse - */ - public function period(BudgetRepositoryInterface $repository, Budget $budget, Carbon $start, Carbon $end, Collection $accounts) + public function period(Budget $budget, Collection $accounts, Carbon $start, Carbon $end) { // chart properties for cache: $cache = new CacheProperties(); @@ -274,140 +225,207 @@ class BudgetController extends Controller $cache->addProperty($end); $cache->addProperty($accounts); $cache->addProperty($budget->id); - $cache->addProperty('budget'); - $cache->addProperty('period'); + $cache->addProperty('chart.budget.period'); if ($cache->has()) { return Response::json($cache->get()); } - // loop over period, add by users range: - $current = clone $start; - $viewRange = Preferences::get('viewRange', '1M')->data; - $set = new Collection; - $repetitions = $repository->getAllBudgetLimitRepetitions($start, $end); + $periods = Navigation::listOfPeriods($start, $end); + $entries = $this->repository->getBudgetPeriodReport(new Collection([$budget]), $accounts, $start, $end); // get the expenses + $budgeted = $this->getBudgetedInPeriod($budget, $start, $end); + // join them into one set of data: + $chartData = [ + ['label' => strval(trans('firefly.spent')), 'type' => 'bar', 'entries' => [],], + ['label' => strval(trans('firefly.budgeted')), 'type' => 'bar', 'entries' => [],], + ]; - while ($current < $end) { - $currentStart = clone $current; - $currentEnd = Navigation::endOfPeriod($currentStart, $viewRange); - $reps = $repetitions->filter( - function (LimitRepetition $repetition) use ($budget, $currentStart) { - if ($repetition->budget_id === $budget->id && $repetition->startdate == $currentStart) { - return true; - } - - return false; - } - ); - $budgeted = $reps->sum('amount'); - $spent = $repository->spentInPeriod(new Collection([$budget]), $accounts, $currentStart, $currentEnd); - $entry = [ - 'date' => clone $currentStart, - 'budgeted' => $budgeted, - 'spent' => $spent, - ]; - $set->push($entry); - $currentEnd->addDay(); - $current = clone $currentEnd; - + foreach (array_keys($periods) as $period) { + $label = $periods[$period]; + $spent = isset($entries[$budget->id]['entries'][$period]) ? $entries[$budget->id]['entries'][$period] : '0'; + $limit = isset($budgeted[$period]) ? $budgeted[$period] : 0; + $chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 12); + $chartData[1]['entries'][$label] = $limit; } - $data = $this->generator->period($set, $viewRange); + $data = $this->generator->multiSet($chartData); $cache->store($data); return Response::json($data); } /** - * @param Collection $repetitions + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function periodNoBudget(Collection $accounts, Carbon $start, Carbon $end) + { + // chart properties for cache: + $cache = new CacheProperties(); + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($accounts); + $cache->addProperty('chart.budget.no-budget'); + if ($cache->has()) { + return Response::json($cache->get()); + } + + // the expenses: + $periods = Navigation::listOfPeriods($start, $end); + $entries = $this->repository->getNoBudgetPeriodReport($accounts, $start, $end); + $chartData = []; + + // join them: + foreach (array_keys($periods) as $period) { + $label = $periods[$period]; + $spent = isset($entries['entries'][$period]) ? $entries['entries'][$period] : '0'; + $chartData[$label] = bcmul($spent, '-1'); + } + $data = $this->generator->singleSet(strval(trans('firefly.spent')), $chartData); + $cache->store($data); + + return Response::json($data); + } + + /** + * @param Budget $budget + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + private function getBudgetedInPeriod(Budget $budget, Carbon $start, Carbon $end): array + { + $key = Navigation::preferredCarbonFormat($start, $end); + $range = Navigation::preferredRangeFormat($start, $end); + $current = clone $start; + $budgeted = []; + while ($current < $end) { + $currentStart = Navigation::startOfPeriod($current, $range); + $currentEnd = Navigation::endOfPeriod($current, $range); + $budgetLimits = $this->repository->getBudgetLimits($budget, $currentStart, $currentEnd); + $index = $currentStart->format($key); + $budgeted[$index] = $budgetLimits->sum('amount'); + $currentEnd->addDay(); + $current = clone $currentEnd; + } + + return $budgeted; + } + + /** + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's 6 but ok. + * + * @param Collection $limits * @param Budget $budget * @param Carbon $start * @param Carbon $end * - * @return Collection + * @return array */ - private function filterRepetitions(Collection $repetitions, Budget $budget, Carbon $start, Carbon $end): Collection + private function getExpensesForBudget(Collection $limits, Budget $budget, Carbon $start, Carbon $end): array { - - return $repetitions->filter( - function (LimitRepetition $repetition) use ($budget, $start, $end) { - if ($repetition->startdate < $end && $repetition->enddate > $start && $repetition->budget_id === $budget->id) { - return true; - } - - return false; + $return = []; + if ($limits->count() === 0) { + $spent = $this->repository->spentInPeriod(new Collection([$budget]), new Collection, $start, $end); + if (bccomp($spent, '0') !== 0) { + $return[$budget->name]['spent'] = bcmul($spent, '-1'); + $return[$budget->name]['left'] = 0; + $return[$budget->name]['overspent'] = 0; } - ); - } - /** - * @param BudgetRepositoryInterface $repository - * @param Budget $budget - * @param Collection $repetitions - * - * @return Collection - */ - private function spentInPeriodMulti(BudgetRepositoryInterface $repository, Budget $budget, Collection $repetitions): Collection - { - $format = strval(trans('config.month_and_day')); - $collection = new Collection; - $name = $budget->name; - /** @var LimitRepetition $repetition */ - foreach ($repetitions as $repetition) { - $expenses = $repository->spentInPeriod(new Collection([$budget]), new Collection, $repetition->startdate, $repetition->enddate); - - if ($repetitions->count() > 1) { - $name = $budget->name . ' ' . trans( - 'firefly.between_dates', - ['start' => $repetition->startdate->formatLocalized($format), 'end' => $repetition->enddate->formatLocalized($format)] - ); - } - $amount = $repetition->amount; - $left = bccomp(bcadd($amount, $expenses), '0') < 1 ? '0' : bcadd($amount, $expenses); - $spent = bccomp(bcadd($amount, $expenses), '0') < 1 ? bcmul($amount, '-1') : $expenses; - $overspent = bccomp(bcadd($amount, $expenses), '0') < 1 ? bcadd($amount, $expenses) : '0'; - $array = [$name, $left, $spent, $overspent, $amount, $spent]; - $collection->push($array); + return $return; } - return $collection; + $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'); + } + } + unset($rows, $row); + + return $return; } /** - * @param BudgetRepositoryInterface $repository - * @param Budget $budget - * @param Carbon $start - * @param Carbon $end + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. * - * @return Collection - */ - private function spentInPeriodSingle(BudgetRepositoryInterface $repository, Budget $budget, Carbon $start, Carbon $end): Collection - { - $collection = new Collection; - $amount = '0'; - $left = '0'; - $spent = $repository->spentInPeriod(new Collection([$budget]), new Collection, $start, $end); - $overspent = '0'; - $array = [$budget->name, $left, $spent, $overspent, $amount, $spent]; - $collection->push($array); - - return $collection; - } - - /** - * @param BudgetRepositoryInterface $repository - * @param Carbon $start - * @param Carbon $end + * Returns an array with the following values: + * 0 => + * 'name' => name of budget + repetition + * 'left' => left in budget repetition (always zero) + * 'overspent' => spent more than budget repetition? (always zero) + * 'spent' => actually spent in period for budget + * 1 => (etc) + * + * @param Budget $budget + * @param Collection $limits * * @return array */ - private function spentInPeriodWithout(BudgetRepositoryInterface $repository, Carbon $start, Carbon $end):array + private function spentInPeriodMulti(Budget $budget, Collection $limits): array { - $list = $repository->journalsInPeriodWithoutBudget(new Collection, $start, $end); // budget - $sum = '0'; - /** @var TransactionJournal $entry */ - foreach ($list as $entry) { - $sum = bcadd(TransactionJournal::amount($entry), $sum); + $return = []; + $format = strval(trans('config.month_and_day')); + $name = $budget->name; + /** @var BudgetLimit $budgetLimit */ + foreach ($limits as $budgetLimit) { + $expenses = $this->repository->spentInPeriod(new Collection([$budget]), new Collection, $budgetLimit->start_date, $budgetLimit->end_date); + + if ($limits->count() > 1) { + $name = $budget->name . ' ' . trans( + 'firefly.between_dates', + [ + 'start' => $budgetLimit->start_date->formatLocalized($format), + 'end' => $budgetLimit->end_date->formatLocalized($format), + ] + ); + } + $amount = $budgetLimit->amount; + $left = bccomp(bcadd($amount, $expenses), '0') < 1 ? '0' : bcadd($amount, $expenses); + $spent = $expenses; + $overspent = bccomp(bcadd($amount, $expenses), '0') < 1 ? bcadd($amount, $expenses) : '0'; + $return[$name] = [ + 'left' => $left, + 'overspent' => $overspent, + 'spent' => $spent, + ]; } - return [trans('firefly.no_budget'), '0', '0', $sum, '0', '0']; + return $return; + } + + /** + * Returns an array with the following values: + * 'name' => "no budget" in local language + * 'repetition_left' => left in budget repetition (always zero) + * 'repetition_overspent' => spent more than budget repetition? (always zero) + * 'spent' => actually spent in period for budget + * + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + private function spentInPeriodWithout(Carbon $start, Carbon $end): string + { + // collector + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $types = [TransactionType::WITHDRAWAL]; + $collector->setAllAssetAccounts()->setTypes($types)->setRange($start, $end)->withoutBudget(); + $journals = $collector->getJournals(); + $sum = '0'; + /** @var Transaction $entry */ + foreach ($journals as $entry) { + $sum = bcadd($entry->transaction_amount, $sum); + } + + return $sum; } } diff --git a/app/Http/Controllers/Chart/BudgetReportController.php b/app/Http/Controllers/Chart/BudgetReportController.php new file mode 100644 index 0000000000..d570eae6b8 --- /dev/null +++ b/app/Http/Controllers/Chart/BudgetReportController.php @@ -0,0 +1,288 @@ +middleware( + function ($request, $next) { + $this->generator = app(GeneratorInterface::class); + $this->budgetRepository = app(BudgetRepositoryInterface::class); + $this->accountRepository = app(AccountRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * @param Collection $accounts + * @param Collection $budgets + * @param Carbon $start + * @param Carbon $end + * @param string $others + * + * @return \Illuminate\Http\JsonResponse + */ + public function accountExpense(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end, string $others) + { + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setBudgets($budgets); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(intval($others) === 1); + $chartData = $helper->generate('expense', 'account'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + + } + + /** + * @param Collection $accounts + * @param Collection $budgets + * @param Carbon $start + * @param Carbon $end + * @param string $others + * + * @return \Illuminate\Http\JsonResponse + */ + public function budgetExpense(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end, string $others) + { + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setBudgets($budgets); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(intval($others) === 1); + $chartData = $helper->generate('expense', 'budget'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + } + + /** + * @param Collection $accounts + * @param Collection $budgets + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function mainChart(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty('chart.budget.report.main'); + $cache->addProperty($accounts); + $cache->addProperty($budgets); + $cache->addProperty($start); + $cache->addProperty($end); + if ($cache->has()) { + return Response::json($cache->get()); + } + /** @var BudgetRepositoryInterface $repository */ + $repository = app(BudgetRepositoryInterface::class); + $format = Navigation::preferredCarbonLocalizedFormat($start, $end); + $function = Navigation::preferredEndOfPeriod($start, $end); + $chartData = []; + $currentStart = clone $start; + + // prep chart data: + foreach ($budgets as $budget) { + $chartData[$budget->id] = [ + 'label' => strval(trans('firefly.spent_in_specific_budget', ['budget' => $budget->name])), + 'type' => 'bar', + 'yAxisID' => 'y-axis-0', + 'entries' => [], + ]; + $chartData[$budget->id . '-sum'] = [ + 'label' => strval(trans('firefly.sum_of_expenses_in_budget', ['budget' => $budget->name])), + 'type' => 'line', + 'fill' => false, + 'yAxisID' => 'y-axis-1', + 'entries' => [], + ]; + $chartData[$budget->id . '-left'] = [ + 'label' => strval(trans('firefly.left_in_budget_limit', ['budget' => $budget->name])), + 'type' => 'bar', + 'fill' => false, + 'yAxisID' => 'y-axis-0', + 'entries' => [], + ]; + } + $allBudgetLimits = $repository->getAllBudgetLimits($start, $end); + $sumOfExpenses = []; + $leftOfLimits = []; + while ($currentStart < $end) { + $currentEnd = clone $currentStart; + $currentEnd = $currentEnd->$function(); + $expenses = $this->groupByBudget($this->getExpenses($accounts, $budgets, $currentStart, $currentEnd)); + $label = $currentStart->formatLocalized($format); + + /** @var Budget $budget */ + foreach ($budgets as $budget) { + // get budget limit(s) for this period): + $budgetLimits = $this->filterBudgetLimits($allBudgetLimits, $budget, $currentStart, $currentEnd); + $currentExpenses = $expenses[$budget->id] ?? '0'; + $sumOfExpenses[$budget->id] = $sumOfExpenses[$budget->id] ?? '0'; + $sumOfExpenses[$budget->id] = bcadd($currentExpenses, $sumOfExpenses[$budget->id]); + $chartData[$budget->id]['entries'][$label] = bcmul($currentExpenses, '-1'); + $chartData[$budget->id . '-sum']['entries'][$label] = bcmul($sumOfExpenses[$budget->id], '-1'); + + if (count($budgetLimits) > 0) { + $budgetLimitId = $budgetLimits->first()->id; + $leftOfLimits[$budgetLimitId] = $leftOfLimits[$budgetLimitId] ?? strval($budgetLimits->sum('amount')); + $leftOfLimits[$budgetLimitId] = bcadd($leftOfLimits[$budgetLimitId], $currentExpenses); + $chartData[$budget->id . '-left']['entries'][$label] = $leftOfLimits[$budgetLimitId]; + } + } + $currentStart = clone $currentEnd; + $currentStart->addDay(); + } + + $data = $this->generator->multiSet($chartData); + $cache->store($data); + + return Response::json($data); + } + + /** + * Returns the budget limits belonging to the given budget and valid on the given day. + * + * @param Collection $budgetLimits + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + private function filterBudgetLimits(Collection $budgetLimits, Budget $budget, Carbon $start, Carbon $end): Collection + { + $set = $budgetLimits->filter( + function (BudgetLimit $budgetLimit) use ($budget, $start, $end) { + if ($budgetLimit->budget_id === $budget->id + && $budgetLimit->start_date->lte($start) // start of budget limit is on or before start + && $budgetLimit->end_date->gte($end) // end of budget limit is on or after end + ) { + return $budgetLimit; + } + + return false; + } + ); + + return $set; + } + + /** + * @param Collection $accounts + * @param Collection $budgets + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + private function getExpenses(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end): Collection + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) + ->setBudgets($budgets)->withOpposingAccount()->disableFilter(); + $accountIds = $accounts->pluck('id')->toArray(); + $transactions = $collector->getJournals(); + $set = MonthReportGenerator::filterExpenses($transactions, $accountIds); + + return $set; + } + + /** + * @param Collection $set + * + * @return array + */ + private function groupByBudget(Collection $set): array + { + // group by category ID: + $grouped = []; + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $jrnlBudId = intval($transaction->transaction_journal_budget_id); + $transBudId = intval($transaction->transaction_budget_id); + $budgetId = max($jrnlBudId, $transBudId); + $grouped[$budgetId] = $grouped[$budgetId] ?? '0'; + $grouped[$budgetId] = bcadd($transaction->transaction_amount, $grouped[$budgetId]); + } + + return $grouped; + } + + /** + * @param Collection $set + * + * @return array + */ + private function groupByOpposingAccount(Collection $set): array + { + $grouped = []; + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $accountId = $transaction->opposing_account_id; + $grouped[$accountId] = $grouped[$accountId] ?? '0'; + $grouped[$accountId] = bcadd($transaction->transaction_amount, $grouped[$accountId]); + } + + return $grouped; + } +} diff --git a/app/Http/Controllers/Chart/CategoryController.php b/app/Http/Controllers/Chart/CategoryController.php index 98a8392ca4..217d122bc8 100644 --- a/app/Http/Controllers/Chart/CategoryController.php +++ b/app/Http/Controllers/Chart/CategoryController.php @@ -15,7 +15,7 @@ namespace FireflyIII\Http\Controllers\Chart; use Carbon\Carbon; -use FireflyIII\Generator\Chart\Category\CategoryChartGeneratorInterface; +use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\AccountType; use FireflyIII\Models\Category; @@ -26,7 +26,6 @@ use Illuminate\Support\Collection; use Navigation; use Preferences; use Response; -use stdClass; /** * Class CategoryController @@ -35,7 +34,7 @@ use stdClass; */ class CategoryController extends Controller { - /** @var CategoryChartGeneratorInterface */ + /** @var GeneratorInterface */ protected $generator; /** @@ -45,10 +44,9 @@ class CategoryController extends Controller { parent::__construct(); // create chart generator: - $this->generator = app(CategoryChartGeneratorInterface::class); + $this->generator = app(GeneratorInterface::class); } - /** * Show an overview for a category for all time, per month/week/year. * @@ -60,34 +58,47 @@ class CategoryController extends Controller */ public function all(CRI $repository, AccountRepositoryInterface $accountRepository, Category $category) { - $start = $repository->firstUseDate($category); - $range = Preferences::get('viewRange', '1M')->data; - $start = Navigation::startOfPeriod($start, $range); - $categoryCollection = new Collection([$category]); - $end = new Carbon; - $entries = new Collection; - $cache = new CacheProperties; - $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('all'); - $cache->addProperty('categories'); + $cache = new CacheProperties; + $cache->addProperty('chart.category.all'); + $cache->addProperty($category->id); if ($cache->has()) { return Response::json($cache->get()); } - while ($start <= $end) { - $currentEnd = Navigation::endOfPeriod($start, $range); - $spent = $repository->spentInPeriod($categoryCollection, $accounts, $start, $currentEnd); - $earned = $repository->earnedInPeriod($categoryCollection, $accounts, $start, $currentEnd); - $date = Navigation::periodShow($start, $range); - $entries->push([clone $start, $date, $spent, $earned]); - $start = Navigation::addPeriod($start, $range, 0); + $start = $repository->firstUseDate($category); + + if ($start->year == 1900) { + $start = new Carbon; } - $entries = $entries->reverse(); - $entries = $entries->slice(0, 48); - $entries = $entries->reverse(); - $data = $this->generator->all($entries); + + $range = Preferences::get('viewRange', '1M')->data; + $start = Navigation::startOfPeriod($start, $range); + $end = new Carbon; + $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + $chartData = [ + [ + 'label' => strval(trans('firefly.spent')), + 'entries' => [], + 'type' => 'bar', + ], + [ + 'label' => strval(trans('firefly.earned')), + 'entries' => [], + 'type' => 'bar', + ], + ]; + + while ($start <= $end) { + $currentEnd = Navigation::endOfPeriod($start, $range); + $spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $start, $currentEnd); + $earned = $repository->earnedInPeriod(new Collection([$category]), $accounts, $start, $currentEnd); + $label = Navigation::periodShow($start, $range); + $chartData[0]['entries'][$label] = bcmul($spent, '-1'); + $chartData[1]['entries'][$label] = $earned; + $start = Navigation::addPeriod($start, $range, 0); + } + + $data = $this->generator->multiSet($chartData); $cache->store($data); return Response::json($data); @@ -123,106 +134,126 @@ class CategoryController extends Controller $cache = new CacheProperties; $cache->addProperty($start); $cache->addProperty($end); - $cache->addProperty('category'); - $cache->addProperty('frontpage'); + $cache->addProperty('chart.category.frontpage'); if ($cache->has()) { return Response::json($cache->get()); } + $chartData = []; $categories = $repository->getCategories(); $accounts = $accountRepository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]); - $set = new Collection; /** @var Category $category */ foreach ($categories as $category) { $spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $start, $end); if (bccomp($spent, '0') === -1) { - $category->spent = $spent; - $set->push($category); + $chartData[$category->name] = bcmul($spent, '-1'); } } - // this is a "fake" entry for the "no category" entry. - $entry = new stdClass; - $entry->name = trans('firefly.no_category'); - $entry->spent = $repository->spentInPeriodWithoutCategory(new Collection, $start, $end); - $set->push($entry); - $set = $set->sortBy('spent'); - $data = $this->generator->frontpage($set); + $chartData[strval(trans('firefly.no_category'))] = bcmul($repository->spentInPeriodWithoutCategory(new Collection, $start, $end), '-1'); + + // sort + arsort($chartData); + + $data = $this->generator->singleSet(strval(trans('firefly.spent')), $chartData); $cache->store($data); return Response::json($data); - } /** + * @param CRI $repository + * @param Category $category + * @param Collection $accounts * @param Carbon $start * @param Carbon $end - * @param Collection $accounts - * @param Collection $categories * - * @return \Illuminate\Http\JsonResponse + * @return \Illuminate\Http\JsonResponse|mixed */ - public function multiYear(Carbon $start, Carbon $end, Collection $accounts, Collection $categories) + public function reportPeriod(CRI $repository, Category $category, Collection $accounts, Carbon $start, Carbon $end) { - - /** @var CRI $repository */ - $repository = app(CRI::class); - - // chart properties for cache: - $cache = new CacheProperties(); + $cache = new CacheProperties; $cache->addProperty($start); $cache->addProperty($end); - $cache->addProperty($accounts); - $cache->addProperty($categories); - $cache->addProperty('multiYearCategory'); - + $cache->addProperty('chart.category.period'); + $cache->addProperty($accounts->pluck('id')->toArray()); + $cache->addProperty($category); if ($cache->has()) { - return Response::json($cache->get()); + return $cache->get(); + } + $expenses = $repository->periodExpenses(new Collection([$category]), $accounts, $start, $end); + $income = $repository->periodIncome(new Collection([$category]), $accounts, $start, $end); + $periods = Navigation::listOfPeriods($start, $end); + $chartData = [ + [ + 'label' => strval(trans('firefly.spent')), + 'entries' => [], + 'type' => 'bar', + ], + [ + 'label' => strval(trans('firefly.earned')), + 'entries' => [], + 'type' => 'bar', + ], + ]; + + foreach (array_keys($periods) as $period) { + $label = $periods[$period]; + $spent = $expenses[$category->id]['entries'][$period] ?? '0'; + $chartData[0]['entries'][$label] = bcmul($spent, '-1'); + $chartData[1]['entries'][$label] = $income[$category->id]['entries'][$period] ?? '0'; } - $entries = new Collection; - - /** @var Category $category */ - foreach ($categories as $category) { - $entry = ['name' => '', 'spent' => [], 'earned' => []]; - - $currentStart = clone $start; - while ($currentStart < $end) { - // fix the date: - $year = $currentStart->year; - $currentEnd = clone $currentStart; - $currentEnd->endOfYear(); - - // get data: - if (is_null($category->id)) { - $entry['name'] = trans('firefly.noCategory'); - $entry['spent'][$year] = ($repository->spentInPeriodWithoutCategory($accounts, $currentStart, $currentEnd) * -1); - $entry['earned'][$year] = $repository->earnedInPeriodWithoutCategory($accounts, $currentStart, $currentEnd); - - // jump to next year. - $currentStart = clone $currentEnd; - $currentStart->addDay(); - continue; - - } - // alternative is a normal category: - $entry['name'] = $category->name; - $entry['spent'][$year] = ($repository->spentInPeriod(new Collection([$category]), $accounts, $currentStart, $currentEnd) * -1); - $entry['earned'][$year] = $repository->earnedInPeriod(new Collection([$category]), $accounts, $currentStart, $currentEnd); - - // jump to next year. - $currentStart = clone $currentEnd; - $currentStart->addDay(); - } - $entries->push($entry); - } - - // generate chart with data: - $data = $this->generator->multiYear($entries); + $data = $this->generator->multiSet($chartData); $cache->store($data); return Response::json($data); + } + /** + * @param CRI $repository + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function reportPeriodNoCategory(CRI $repository, Collection $accounts, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('chart.category.period.no-cat'); + $cache->addProperty($accounts->pluck('id')->toArray()); + if ($cache->has()) { + return $cache->get(); + } + $expenses = $repository->periodExpensesNoCategory($accounts, $start, $end); + $income = $repository->periodIncomeNoCategory($accounts, $start, $end); + $periods = Navigation::listOfPeriods($start, $end); + $chartData = [ + [ + 'label' => strval(trans('firefly.spent')), + 'entries' => [], + 'type' => 'bar', + ], + [ + 'label' => strval(trans('firefly.earned')), + 'entries' => [], + 'type' => 'bar', + ], + ]; + foreach (array_keys($periods) as $period) { + $label = $periods[$period]; + $spent = $expenses['entries'][$period] ?? '0'; + $chartData[0]['entries'][$label] = bcmul($spent, '-1'); + $chartData[1]['entries'][$label] = $income['entries'][$period] ?? '0'; + + } + $data = $this->generator->multiSet($chartData); + $cache->store($data); + + return Response::json($data); } /** @@ -244,6 +275,7 @@ class CategoryController extends Controller return Response::json($data); } + /** * @param CRI $repository * @param Category $category @@ -254,33 +286,47 @@ class CategoryController extends Controller */ private function makePeriodChart(CRI $repository, Category $category, Carbon $start, Carbon $end) { - $categoryCollection = new Collection([$category]); - $cache = new CacheProperties; + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($category->id); + $cache->addProperty('chart.category.period-chart'); /** @var AccountRepositoryInterface $accountRepository */ $accountRepository = app(AccountRepositoryInterface::class); $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($accounts); - $cache->addProperty($category->id); - $cache->addProperty('specific-period'); - - if ($cache->has()) { return $cache->get(); } - $entries = new Collection; + + // chart data + $chartData = [ + [ + 'label' => strval(trans('firefly.spent')), + 'entries' => [], + 'type' => 'bar', + ], + [ + 'label' => strval(trans('firefly.earned')), + 'entries' => [], + 'type' => 'bar', + ], + ]; + while ($start <= $end) { - $spent = $repository->spentInPeriod($categoryCollection, $accounts, $start, $start); - $earned = $repository->earnedInPeriod($categoryCollection, $accounts, $start, $start); - $date = Navigation::periodShow($start, '1D'); - $entries->push([clone $start, $date, $spent, $earned]); + $spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $start, $start); + $earned = $repository->earnedInPeriod(new Collection([$category]), $accounts, $start, $start); + $label = Navigation::periodShow($start, '1D'); + + $chartData[0]['entries'][$label] = bcmul($spent, '-1'); + $chartData[1]['entries'][$label] = $earned; + + $start->addDay(); } - $data = $this->generator->period($entries); + $data = $this->generator->multiSet($chartData); $cache->store($data); return $data; diff --git a/app/Http/Controllers/Chart/CategoryReportController.php b/app/Http/Controllers/Chart/CategoryReportController.php new file mode 100644 index 0000000000..94f6cfb370 --- /dev/null +++ b/app/Http/Controllers/Chart/CategoryReportController.php @@ -0,0 +1,352 @@ +middleware( + function ($request, $next) { + $this->generator = app(GeneratorInterface::class); + $this->categoryRepository = app(CategoryRepositoryInterface::class); + $this->accountRepository = app(AccountRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * @param Collection $accounts + * @param Collection $categories + * @param Carbon $start + * @param Carbon $end + * @param string $others + * + * @return \Illuminate\Http\JsonResponse + */ + public function accountExpense(Collection $accounts, Collection $categories, Carbon $start, Carbon $end, string $others) + { + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setCategories($categories); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(intval($others) === 1); + $chartData = $helper->generate('expense', 'account'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + } + + /** + * @param Collection $accounts + * @param Collection $categories + * @param Carbon $start + * @param Carbon $end + * @param string $others + * + * @return \Illuminate\Http\JsonResponse + */ + public function accountIncome(Collection $accounts, Collection $categories, Carbon $start, Carbon $end, string $others) + { + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setCategories($categories); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(intval($others) === 1); + $chartData = $helper->generate('income', 'account'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + } + + /** + * @param Collection $accounts + * @param Collection $categories + * @param Carbon $start + * @param Carbon $end + * @param string $others + * + * @return \Illuminate\Http\JsonResponse + */ + public function categoryExpense(Collection $accounts, Collection $categories, Carbon $start, Carbon $end, string $others) + { + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setCategories($categories); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(intval($others) === 1); + $chartData = $helper->generate('expense', 'category'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + } + + /** + * @param Collection $accounts + * @param Collection $categories + * @param Carbon $start + * @param Carbon $end + * @param string $others + * + * @return \Illuminate\Http\JsonResponse + */ + public function categoryIncome(Collection $accounts, Collection $categories, Carbon $start, Carbon $end, string $others) + { + + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setCategories($categories); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(intval($others) === 1); + $chartData = $helper->generate('income', 'category'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + } + + /** + * @param Collection $accounts + * @param Collection $categories + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function mainChart(Collection $accounts, Collection $categories, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty('chart.category.report.main'); + $cache->addProperty($accounts); + $cache->addProperty($categories); + $cache->addProperty($start); + $cache->addProperty($end); + if ($cache->has()) { + return Response::json($cache->get()); + } + + $format = Navigation::preferredCarbonLocalizedFormat($start, $end); + $function = Navigation::preferredEndOfPeriod($start, $end); + $chartData = []; + $currentStart = clone $start; + + // prep chart data: + foreach ($categories as $category) { + $chartData[$category->id . '-in'] = [ + 'label' => $category->name . ' (' . strtolower(strval(trans('firefly.income'))) . ')', + 'type' => 'bar', + 'yAxisID' => 'y-axis-0', + 'entries' => [], + ]; + $chartData[$category->id . '-out'] = [ + 'label' => $category->name . ' (' . strtolower(strval(trans('firefly.expenses'))) . ')', + 'type' => 'bar', + 'yAxisID' => 'y-axis-0', + 'entries' => [], + ]; + // total in, total out: + $chartData[$category->id . '-total-in'] = [ + 'label' => $category->name . ' (' . strtolower(strval(trans('firefly.sum_of_income'))) . ')', + 'type' => 'line', + 'fill' => false, + 'yAxisID' => 'y-axis-1', + 'entries' => [], + ]; + $chartData[$category->id . '-total-out'] = [ + 'label' => $category->name . ' (' . strtolower(strval(trans('firefly.sum_of_expenses'))) . ')', + 'type' => 'line', + 'fill' => false, + 'yAxisID' => 'y-axis-1', + 'entries' => [], + ]; + } + $sumOfIncome = []; + $sumOfExpense = []; + + while ($currentStart < $end) { + $currentEnd = clone $currentStart; + $currentEnd = $currentEnd->$function(); + $expenses = $this->groupByCategory($this->getExpenses($accounts, $categories, $currentStart, $currentEnd)); + $income = $this->groupByCategory($this->getIncome($accounts, $categories, $currentStart, $currentEnd)); + $label = $currentStart->formatLocalized($format); + + /** @var Category $category */ + foreach ($categories as $category) { + $labelIn = $category->id . '-in'; + $labelOut = $category->id . '-out'; + $labelSumIn = $category->id . '-total-in'; + $labelSumOut = $category->id . '-total-out'; + $currentIncome = $income[$category->id] ?? '0'; + $currentExpense = $expenses[$category->id] ?? '0'; + + + // add to sum: + $sumOfIncome[$category->id] = $sumOfIncome[$category->id] ?? '0'; + $sumOfExpense[$category->id] = $sumOfExpense[$category->id] ?? '0'; + $sumOfIncome[$category->id] = bcadd($sumOfIncome[$category->id], $currentIncome); + $sumOfExpense[$category->id] = bcadd($sumOfExpense[$category->id], $currentExpense); + + // add to chart: + $chartData[$labelIn]['entries'][$label] = $currentIncome; + $chartData[$labelOut]['entries'][$label] = $currentExpense; + $chartData[$labelSumIn]['entries'][$label] = $sumOfIncome[$category->id]; + $chartData[$labelSumOut]['entries'][$label] = $sumOfExpense[$category->id]; + } + $currentStart = clone $currentEnd; + $currentStart->addDay(); + } + // remove all empty entries to prevent cluttering: + $newSet = []; + foreach ($chartData as $key => $entry) { + if (!array_sum($entry['entries']) == 0) { + $newSet[$key] = $chartData[$key]; + } + } + if (count($newSet) === 0) { + $newSet = $chartData; + } + $data = $this->generator->multiSet($newSet); + $cache->store($data); + + return Response::json($data); + } + + + /** + * @param Collection $accounts + * @param Collection $categories + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + private function getExpenses(Collection $accounts, Collection $categories, Carbon $start, Carbon $end): Collection + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) + ->setCategories($categories)->withOpposingAccount()->disableFilter(); + $accountIds = $accounts->pluck('id')->toArray(); + $transactions = $collector->getJournals(); + $set = MonthReportGenerator::filterExpenses($transactions, $accountIds); + + return $set; + } + + /** + * @param Collection $accounts + * @param Collection $categories + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + private function getIncome(Collection $accounts, Collection $categories, Carbon $start, Carbon $end): Collection + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) + ->setCategories($categories)->withOpposingAccount(); + $accountIds = $accounts->pluck('id')->toArray(); + $transactions = $collector->getJournals(); + $set = MonthReportGenerator::filterIncome($transactions, $accountIds); + + return $set; + } + + /** + * @param Collection $set + * + * @return array + */ + private function groupByCategory(Collection $set): array + { + // group by category ID: + $grouped = []; + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $jrnlCatId = intval($transaction->transaction_journal_category_id); + $transCatId = intval($transaction->transaction_category_id); + $categoryId = max($jrnlCatId, $transCatId); + $grouped[$categoryId] = $grouped[$categoryId] ?? '0'; + $grouped[$categoryId] = bcadd($transaction->transaction_amount, $grouped[$categoryId]); + } + + return $grouped; + } + + /** + * @param Collection $set + * + * @return array + */ + private function groupByOpposingAccount(Collection $set): array + { + $grouped = []; + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $accountId = $transaction->opposing_account_id; + $grouped[$accountId] = $grouped[$accountId] ?? '0'; + $grouped[$accountId] = bcadd($transaction->transaction_amount, $grouped[$accountId]); + } + + return $grouped; + } +} diff --git a/app/Http/Controllers/Chart/PiggyBankController.php b/app/Http/Controllers/Chart/PiggyBankController.php index bdcf1408f5..aa11d8bbf0 100644 --- a/app/Http/Controllers/Chart/PiggyBankController.php +++ b/app/Http/Controllers/Chart/PiggyBankController.php @@ -13,13 +13,12 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers\Chart; -use FireflyIII\Generator\Chart\PiggyBank\PiggyBankChartGeneratorInterface; +use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBankEvent; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use FireflyIII\Support\CacheProperties; -use Illuminate\Support\Collection; use Response; @@ -31,7 +30,7 @@ use Response; class PiggyBankController extends Controller { - /** @var PiggyBankChartGeneratorInterface */ + /** @var GeneratorInterface */ protected $generator; /** @@ -41,7 +40,7 @@ class PiggyBankController extends Controller { parent::__construct(); // create chart generator: - $this->generator = app(PiggyBankChartGeneratorInterface::class); + $this->generator = app(GeneratorInterface::class); } /** @@ -56,26 +55,24 @@ class PiggyBankController extends Controller { // chart properties for cache: $cache = new CacheProperties; - $cache->addProperty('piggy-history'); + $cache->addProperty('chart.piggy-bank.history'); $cache->addProperty($piggyBank->id); if ($cache->has()) { return Response::json($cache->get()); } - $set = $repository->getEvents($piggyBank); - $set = $set->reverse(); - $collection = []; + $set = $repository->getEvents($piggyBank); + $set = $set->reverse(); + $chartData = []; + $sum = '0'; /** @var PiggyBankEvent $entry */ foreach ($set as $entry) { - $date = $entry->date->format('Y-m-d'); - $amount = $entry->amount; - if (isset($collection[$date])) { - $amount = bcadd($amount, $collection[$date]); - } - $collection[$date] = $amount; + $label = $entry->date->formatLocalized(strval(trans('config.month_and_day'))); + $sum = bcadd($sum, $entry->amount); + $chartData[$label] = $sum; } - $data = $this->generator->history(new Collection($collection)); + $data = $this->generator->singleSet($piggyBank->name, $chartData); $cache->store($data); return Response::json($data); diff --git a/app/Http/Controllers/Chart/ReportController.php b/app/Http/Controllers/Chart/ReportController.php index 941d66da86..03833b4504 100644 --- a/app/Http/Controllers/Chart/ReportController.php +++ b/app/Http/Controllers/Chart/ReportController.php @@ -15,7 +15,7 @@ namespace FireflyIII\Http\Controllers\Chart; use Carbon\Carbon; -use FireflyIII\Generator\Chart\Report\ReportChartGeneratorInterface; +use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Support\CacheProperties; @@ -32,7 +32,7 @@ use Steam; class ReportController extends Controller { - /** @var ReportChartGeneratorInterface */ + /** @var GeneratorInterface */ protected $generator; /** @@ -42,298 +42,184 @@ class ReportController extends Controller { parent::__construct(); // create chart generator: - $this->generator = app(ReportChartGeneratorInterface::class); + $this->generator = app(GeneratorInterface::class); } /** * This chart, by default, is shown on the multi-year and year report pages, * which means that giving it a 2 week "period" should be enough granularity. * - * @param string $reportType + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function netWorth(Collection $accounts, Carbon $start, Carbon $end) + { + // chart properties for cache: + $cache = new CacheProperties; + $cache->addProperty('chart.report.net-worth'); + $cache->addProperty($start); + $cache->addProperty($accounts); + $cache->addProperty($end); + if ($cache->has()) { + return Response::json($cache->get()); + } + $ids = $accounts->pluck('id')->toArray(); + $current = clone $start; + $chartData = []; + while ($current < $end) { + $balances = Steam::balancesById($ids, $current); + $sum = $this->arraySum($balances); + $label = $current->formatLocalized(strval(trans('config.month_and_day'))); + $chartData[$label] = $sum; + $current->addDays(7); + } + + $data = $this->generator->singleSet(strval(trans('firefly.net_worth')), $chartData); + $cache->store($data); + + return Response::json($data); + } + + + /** + * Shows income and expense, debet/credit: operations + * + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * + * @return \Illuminate\Http\JsonResponse + */ + public function operations(Collection $accounts, Carbon $start, Carbon $end) + { + // chart properties for cache: + $cache = new CacheProperties; + $cache->addProperty('chart.report.operations'); + $cache->addProperty($start); + $cache->addProperty($accounts); + $cache->addProperty($end); + if ($cache->has()) { + return Response::json($cache->get()); + } + $format = Navigation::preferredCarbonLocalizedFormat($start, $end); + $source = $this->getChartData($accounts, $start, $end); + $chartData = [ + [ + 'label' => trans('firefly.income'), + 'type' => 'bar', + 'entries' => [], + ], + [ + 'label' => trans('firefly.expenses'), + 'type' => 'bar', + 'entries' => [], + ], + ]; + + foreach ($source['earned'] as $date => $amount) { + $carbon = new Carbon($date); + $label = $carbon->formatLocalized($format); + $earned = $chartData[0]['entries'][$label] ?? '0'; + $chartData[0]['entries'][$label] = bcadd($earned, $amount); + } + foreach ($source['spent'] as $date => $amount) { + $carbon = new Carbon($date); + $label = $carbon->formatLocalized($format); + $spent = $chartData[1]['entries'][$label] ?? '0'; + $chartData[1]['entries'][$label] = bcadd($spent, $amount); + } + + + $data = $this->generator->multiSet($chartData); + $cache->store($data); + + return Response::json($data); + + } + + /** + * Shows sum income and expense, debet/credit: operations + * * @param Carbon $start * @param Carbon $end * @param Collection $accounts * * @return \Illuminate\Http\JsonResponse */ - public function netWorth(string $reportType, Carbon $start, Carbon $end, Collection $accounts) + public function sum(Collection $accounts, Carbon $start, Carbon $end) { - // chart properties for cache: - $cache = new CacheProperties; - $cache->addProperty('netWorth'); - $cache->addProperty($start); - $cache->addProperty($reportType); - $cache->addProperty($accounts); - $cache->addProperty($end); - if ($cache->has()) { - return Response::json($cache->get()); - } - $ids = $accounts->pluck('id')->toArray(); - $current = clone $start; - $entries = new Collection; - while ($current < $end) { - $balances = Steam::balancesById($ids, $current); - $sum = $this->arraySum($balances); - $entries->push( - [ - 'date' => clone $current, - 'net-worth' => $sum, - ] - ); - $current->addDays(7); - } - $data = $this->generator->netWorth($entries); - - $cache->store($data); - - return Response::json($data); - } - - - /** - * @param AccountTaskerInterface $accountTasker - * @param string $reportType - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return \Illuminate\Http\JsonResponse - * @internal param AccountRepositoryInterface $repository - */ - public function yearInOut(AccountTaskerInterface $accountTasker, string $reportType, Carbon $start, Carbon $end, Collection $accounts) - { - // chart properties for cache: - $cache = new CacheProperties; - $cache->addProperty('yearInOut'); - $cache->addProperty($start); - $cache->addProperty($reportType); - $cache->addProperty($accounts); - $cache->addProperty($end); - if ($cache->has()) { - return Response::json($cache->get()); - } - - // always per month. - $currentStart = clone $start; - $spentArray = []; - $earnedArray = []; - while ($currentStart <= $end) { - $currentEnd = Navigation::endOfPeriod($currentStart, '1M'); - $date = $currentStart->format('Y-m'); - $spent = $accountTasker->amountOutInPeriod($accounts, $accounts, $currentStart, $currentEnd); - $earned = $accountTasker->amountInInPeriod($accounts, $accounts, $currentStart, $currentEnd); - $spentArray[$date] = bcmul($spent, '-1'); - $earnedArray[$date] = $earned; - $currentStart = Navigation::addPeriod($currentStart, '1M', 0); - } - - if ($start->diffInMonths($end) > 12) { - // data = method X - $data = $this->multiYearInOut($earnedArray, $spentArray, $start, $end); - $cache->store($data); - - return Response::json($data); - } - - // data = method Y - $data = $this->singleYearInOut($earnedArray, $spentArray, $start, $end); - $cache->store($data); - - return Response::json($data); - - - } - - /** - * @param AccountTaskerInterface $accountTasker - * @param string $reportType - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return \Illuminate\Http\JsonResponse - * @internal param AccountRepositoryInterface $repository - */ - public function yearInOutSummarized(AccountTaskerInterface $accountTasker, string $reportType, Carbon $start, Carbon $end, Collection $accounts) - { // chart properties for cache: $cache = new CacheProperties; - $cache->addProperty('yearInOutSummarized'); + $cache->addProperty('chart.report.sum'); $cache->addProperty($start); $cache->addProperty($end); - $cache->addProperty($reportType); $cache->addProperty($accounts); if ($cache->has()) { return Response::json($cache->get()); } - - // always per month. - $currentStart = clone $start; - $spentArray = []; - $earnedArray = []; - while ($currentStart <= $end) { - $currentEnd = Navigation::endOfPeriod($currentStart, '1M'); - $date = $currentStart->format('Y-m'); - $spent = $accountTasker->amountOutInPeriod($accounts, $accounts, $currentStart, $currentEnd); - $earned = $accountTasker->amountInInPeriod($accounts, $accounts, $currentStart, $currentEnd); - $spentArray[$date] = bcmul($spent, '-1'); - $earnedArray[$date] = $earned; - $currentStart = Navigation::addPeriod($currentStart, '1M', 0); + $source = $this->getChartData($accounts, $start, $end); + $numbers = [ + 'sum_earned' => '0', + 'avg_earned' => '0', + 'count_earned' => 0, + 'sum_spent' => '0', + 'avg_spent' => '0', + 'count_spent' => 0, + ]; + foreach ($source['earned'] as $amount) { + $numbers['sum_earned'] = bcadd($amount, $numbers['sum_earned']); + $numbers['count_earned']++; + } + if ($numbers['count_earned'] > 0) { + $numbers['avg_earned'] = $numbers['sum_earned'] / $numbers['count_earned']; + } + foreach ($source['spent'] as $amount) { + $numbers['sum_spent'] = bcadd($amount, $numbers['sum_spent']); + $numbers['count_spent']++; + } + if ($numbers['count_spent'] > 0) { + $numbers['avg_spent'] = $numbers['sum_spent'] / $numbers['count_spent']; } - if ($start->diffInMonths($end) > 12) { - // per year - $data = $this->multiYearInOutSummarized($earnedArray, $spentArray, $start, $end); - $cache->store($data); + $chartData = [ + [ + 'label' => strval(trans('firefly.income')), + 'type' => 'bar', + 'entries' => [ + strval(trans('firefly.sum_of_period')) => $numbers['sum_earned'], + strval(trans('firefly.average_in_period')) => $numbers['avg_earned'], + ], + ], + [ + 'label' => trans('firefly.expenses'), + 'type' => 'bar', + 'entries' => [ + strval(trans('firefly.sum_of_period')) => $numbers['sum_spent'], + strval(trans('firefly.average_in_period')) => $numbers['avg_spent'], + ], + ], + ]; - return Response::json($data); - } - // per month! - $data = $this->singleYearInOutSummarized($earnedArray, $spentArray, $start, $end); + + $data = $this->generator->multiSet($chartData); $cache->store($data); return Response::json($data); } - /** - * @param array $earned - * @param array $spent - * @param Carbon $start - * @param Carbon $end - * - * @return array - */ - protected function multiYearInOut(array $earned, array $spent, Carbon $start, Carbon $end) - { - $entries = new Collection; - while ($start < $end) { - - $incomeSum = $this->pluckFromArray($start->year, $earned); - $expenseSum = $this->pluckFromArray($start->year, $spent); - - $entries->push([clone $start, $incomeSum, $expenseSum]); - $start->addYear(); - } - - $data = $this->generator->multiYearInOut($entries); - - return $data; - } - - /** - * @param array $earned - * @param array $spent - * @param Carbon $start - * @param Carbon $end - * - * @return array - */ - protected function multiYearInOutSummarized(array $earned, array $spent, Carbon $start, Carbon $end) - { - $income = '0'; - $expense = '0'; - $count = 0; - while ($start < $end) { - - $currentIncome = $this->pluckFromArray($start->year, $earned); - $currentExpense = $this->pluckFromArray($start->year, $spent); - $income = bcadd($income, $currentIncome); - $expense = bcadd($expense, $currentExpense); - - $count++; - $start->addYear(); - } - - $data = $this->generator->multiYearInOutSummarized($income, $expense, $count); - - return $data; - } - - /** - * @param int $year - * @param array $set - * - * @return string - */ - protected function pluckFromArray($year, array $set) - { - $sum = '0'; - foreach ($set as $date => $amount) { - if (substr($date, 0, 4) == $year) { - $sum = bcadd($sum, $amount); - } - } - - return $sum; - - } - - /** - * @param array $earned - * @param array $spent - * @param Carbon $start - * @param Carbon $end - * - * @return array - */ - protected function singleYearInOut(array $earned, array $spent, Carbon $start, Carbon $end) - { - // per month? simply use each month. - - $entries = new Collection; - while ($start < $end) { - // total income and total expenses: - $date = $start->format('Y-m'); - $incomeSum = isset($earned[$date]) ? $earned[$date] : 0; - $expenseSum = isset($spent[$date]) ? $spent[$date] : 0; - - $entries->push([clone $start, $incomeSum, $expenseSum]); - $start->addMonth(); - } - - $data = $this->generator->yearInOut($entries); - - return $data; - } - - /** - * @param array $earned - * @param array $spent - * @param Carbon $start - * @param Carbon $end - * - * @return array - */ - protected function singleYearInOutSummarized(array $earned, array $spent, Carbon $start, Carbon $end) - { - $income = '0'; - $expense = '0'; - $count = 0; - while ($start < $end) { - $date = $start->format('Y-m'); - $currentIncome = isset($earned[$date]) ? $earned[$date] : 0; - $currentExpense = isset($spent[$date]) ? $spent[$date] : 0; - $income = bcadd($income, $currentIncome); - $expense = bcadd($expense, $currentExpense); - - $count++; - $start->addMonth(); - } - - $data = $this->generator->yearInOutSummarized($income, $expense, $count); - - return $data; - } - /** * @param $array * * @return string */ - private function arraySum($array) : string + private function arraySum($array): string { $sum = '0'; foreach ($array as $entry) { @@ -342,4 +228,47 @@ class ReportController extends Controller return $sum; } + + /** + * Collects the incomes and expenses for the given periods, grouped per month. Will cache its results + * + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + private function getChartData(Collection $accounts, Carbon $start, Carbon $end): array + { + $cache = new CacheProperties; + $cache->addProperty('chart.report.get-chart-data'); + $cache->addProperty($start); + $cache->addProperty($accounts); + $cache->addProperty($end); + if ($cache->has()) { + return $cache->get(); + } + + + $tasker = app(AccountTaskerInterface::class); + $currentStart = clone $start; + $spentArray = []; + $earnedArray = []; + while ($currentStart <= $end) { + $currentEnd = Navigation::endOfPeriod($currentStart, '1M'); + $label = $currentStart->format('Y-m') . '-01'; + $spent = $tasker->amountOutInPeriod($accounts, $accounts, $currentStart, $currentEnd); + $earned = $tasker->amountInInPeriod($accounts, $accounts, $currentStart, $currentEnd); + $spentArray[$label] = bcmul($spent, '-1'); + $earnedArray[$label] = $earned; + $currentStart = Navigation::addPeriod($currentStart, '1M', 0); + } + $result = [ + 'spent' => $spentArray, + 'earned' => $earnedArray, + ]; + $cache->store($result); + + return $result; + } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php old mode 100755 new mode 100644 index 6101c947f6..bbf6cb51d5 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -13,11 +13,17 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers; -use Carbon\Carbon; +use FireflyConfig; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Routing\Controller as BaseController; +use Session; +use URL; use View; /** @@ -45,39 +51,86 @@ class Controller extends BaseController View::share('hideCategories', false); View::share('hideBills', false); View::share('hideTags', false); + $isDemoSite = FireflyConfig::get('is_demo_site', config('firefly.configuration.is_demo_site'))->data; + View::share('IS_DEMO_SITE', $isDemoSite); + View::share('DEMO_USERNAME', env('DEMO_USERNAME', '')); + View::share('DEMO_PASSWORD', env('DEMO_PASSWORD', '')); - // save some formats: - $this->monthFormat = (string)trans('config.month'); - $this->monthAndDayFormat = (string)trans('config.month_and_day'); - $this->dateTimeFormat = (string)trans('config.date_time'); + // translations: + $this->middleware( + function ($request, $next) { + $this->monthFormat = (string)trans('config.month'); + $this->monthAndDayFormat = (string)trans('config.month_and_day'); + $this->dateTimeFormat = (string)trans('config.date_time'); + + return $next($request); + } + ); } /** - * Take the array as returned by CategoryRepositoryInterface::spentPerDay and CategoryRepositoryInterface::earnedByDay - * and sum up everything in the array in the given range. + * Functionality: * - * @param Carbon $start - * @param Carbon $end - * @param array $array + * - If the $identifier contains the word "delete" then a remembered uri with the text "/show/" in it will not be returned but instead the index (/) + * will be returned. + * - If the remembered uri contains "javascript/" the remembered uri will not be returned but instead the index (/) will be returned. + * + * @param string $identifier * * @return string */ - protected function getSumOfRange(Carbon $start, Carbon $end, array $array) + protected function getPreviousUri(string $identifier): string { - $sum = '0'; - $currentStart = clone $start; // to not mess with the original one - $currentEnd = clone $end; // to not mess with the original one - - while ($currentStart <= $currentEnd) { - $date = $currentStart->format('Y-m-d'); - if (isset($array[$date])) { - $sum = bcadd($sum, $array[$date]); - } - $currentStart->addDay(); + $uri = strval(session($identifier)); + if (!(strpos($identifier, 'delete') === false) && !(strpos($uri, '/show/') === false)) { + $uri = route('index'); + } + if (!(strpos($uri, 'javascript') === false)) { + $uri = route('index'); } - return $sum; + return $uri; + } + + /** + * @param TransactionJournal $journal + * + * @return bool + */ + protected function isOpeningBalance(TransactionJournal $journal): bool + { + return TransactionJournal::transactionTypeStr($journal) === TransactionType::OPENING_BALANCE; + } + + /** + * @param TransactionJournal $journal + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + protected function redirectToAccount(TransactionJournal $journal) + { + $valid = [AccountType::DEFAULT, AccountType::ASSET]; + $transactions = $journal->transactions; + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $account = $transaction->account; + if (in_array($account->accountType->type, $valid)) { + return redirect(route('accounts.show', [$account->id])); + } + + } + Session::flash('error', strval(trans('firefly.cannot_redirect_to_account'))); + + return redirect(route('index')); + } + + /** + * @param string $identifier + */ + protected function rememberPreviousUri(string $identifier) + { + Session::put($identifier, URL::previous()); } } diff --git a/app/Http/Controllers/CurrencyController.php b/app/Http/Controllers/CurrencyController.php index 1b3e817aa7..01cb6abd3e 100644 --- a/app/Http/Controllers/CurrencyController.php +++ b/app/Http/Controllers/CurrencyController.php @@ -17,11 +17,9 @@ use Cache; use FireflyIII\Http\Requests\CurrencyFormRequest; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; -use Input; use Log; use Preferences; use Session; -use URL; use View; /** @@ -39,8 +37,16 @@ class CurrencyController extends Controller public function __construct() { parent::__construct(); - View::share('title', trans('firefly.currencies')); - View::share('mainTitleIcon', 'fa-usd'); + + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.currencies')); + View::share('mainTitleIcon', 'fa-usd'); + + return $next($request); + } + ); } /** @@ -52,14 +58,14 @@ class CurrencyController extends Controller $subTitle = trans('firefly.create_currency'); // put previous url in session if not redirect from store (not "create another"). - if (session('currency.create.fromStore') !== true) { - Session::put('currency.create.url', URL::previous()); + if (session('currencies.create.fromStore') !== true) { + $this->rememberPreviousUri('currencies.create.uri'); } - Session::forget('currency.create.fromStore'); + Session::forget('currencies.create.fromStore'); Session::flash('gaEventCategory', 'currency'); Session::flash('gaEventAction', 'create'); - return view('currency.create', compact('subTitleIcon', 'subTitle')); + return view('currencies.create', compact('subTitleIcon', 'subTitle')); } /** @@ -77,54 +83,54 @@ class CurrencyController extends Controller Cache::forget('FFCURRENCYSYMBOL'); Cache::forget('FFCURRENCYCODE'); - return redirect(route('currency.index')); + return redirect(route('currencies.index')); } + /** - * @param TransactionCurrency $currency + * @param CurrencyRepositoryInterface $repository + * @param TransactionCurrency $currency * - * @return \Illuminate\Http\RedirectResponse|View + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View */ - public function delete(TransactionCurrency $currency) + public function delete(CurrencyRepositoryInterface $repository, TransactionCurrency $currency) { - if (!$this->canDeleteCurrency($currency)) { + if (!$repository->canDeleteCurrency($currency)) { Session::flash('error', trans('firefly.cannot_delete_currency', ['name' => $currency->name])); - return redirect(route('currency.index')); + return redirect(route('currencies.index')); } // put previous url in session - Session::put('currency.delete.url', URL::previous()); + $this->rememberPreviousUri('currencies.delete.uri'); Session::flash('gaEventCategory', 'currency'); Session::flash('gaEventAction', 'delete'); $subTitle = trans('form.delete_currency', ['name' => $currency->name]); - return view('currency.delete', compact('currency', 'subTitle')); + return view('currencies.delete', compact('currency', 'subTitle')); } /** - * @param TransactionCurrency $currency + * @param CurrencyRepositoryInterface $repository + * @param TransactionCurrency $currency * - * @return \Illuminate\Http\RedirectResponse - * @throws \Exception + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function destroy(TransactionCurrency $currency) + public function destroy(CurrencyRepositoryInterface $repository, TransactionCurrency $currency) { - if (!$this->canDeleteCurrency($currency)) { + if (!$repository->canDeleteCurrency($currency)) { Session::flash('error', trans('firefly.cannot_delete_currency', ['name' => $currency->name])); - return redirect(route('currency.index')); + return redirect(route('currencies.index')); } + $repository->destroy($currency); Session::flash('success', trans('firefly.deleted_currency', ['name' => $currency->name])); - if (auth()->user()->hasRole('owner')) { - $currency->delete(); - } - return redirect(session('currency.delete.url')); + return redirect($this->getPreviousUri('currencies.delete.uri')); } /** @@ -139,14 +145,14 @@ class CurrencyController extends Controller $currency->symbol = htmlentities($currency->symbol); // put previous url in session if not redirect from store (not "return_to_edit"). - if (session('currency.edit.fromUpdate') !== true) { - Session::put('currency.edit.url', URL::previous()); + if (session('currencies.edit.fromUpdate') !== true) { + $this->rememberPreviousUri('currencies.edit.uri'); } - Session::forget('currency.edit.fromUpdate'); + Session::forget('currencies.edit.fromUpdate'); Session::flash('gaEventCategory', 'currency'); Session::flash('gaEventAction', 'edit'); - return view('currency.edit', compact('currency', 'subTitle', 'subTitleIcon')); + return view('currencies.edit', compact('currency', 'subTitle', 'subTitleIcon')); } @@ -162,11 +168,11 @@ class CurrencyController extends Controller if (!auth()->user()->hasRole('owner')) { - Session::flash('warning', trans('firefly.ask_site_owner', ['owner' => env('SITE_OWNER')])); + Session::flash('info', trans('firefly.ask_site_owner', ['owner' => env('SITE_OWNER')])); } - return view('currency.index', compact('currencies', 'defaultCurrency')); + return view('currencies.index', compact('currencies', 'defaultCurrency')); } /** @@ -181,23 +187,20 @@ class CurrencyController extends Controller if (!auth()->user()->hasRole('owner')) { Log::error('User ' . auth()->user()->id . ' is not admin, but tried to store a currency.'); - return redirect(session('currency.create.url')); + return redirect($this->getPreviousUri('currencies.create.uri')); } $data = $request->getCurrencyData(); $currency = $repository->store($data); Session::flash('success', trans('firefly.created_currency', ['name' => $currency->name])); - if (intval(Input::get('create_another')) === 1) { - Session::put('currency.create.fromStore', true); + if (intval($request->get('create_another')) === 1) { + Session::put('currencies.create.fromStore', true); - return redirect(route('currency.create'))->withInput(); + return redirect(route('currencies.create'))->withInput(); } - // redirect to previous URL. - return redirect(session('currency.create.url')); - - + return redirect($this->getPreviousUri('currencies.create.uri')); } /** @@ -217,50 +220,12 @@ class CurrencyController extends Controller Preferences::mark(); - if (intval(Input::get('return_to_edit')) === 1) { - Session::put('currency.edit.fromUpdate', true); + if (intval($request->get('return_to_edit')) === 1) { + Session::put('currencies.edit.fromUpdate', true); - return redirect(route('currency.edit', [$currency->id])); + return redirect(route('currencies.edit', [$currency->id])); } - // redirect to previous URL. - return redirect(session('currency.edit.url')); - + return redirect($this->getPreviousUri('currencies.edit.uri')); } - - /** - * @param TransactionCurrency $currency - * - * @return bool - */ - private function canDeleteCurrency(TransactionCurrency $currency): bool - { - $repository = app(CurrencyRepositoryInterface::class); - - // has transactions still - if ($repository->countJournals($currency) > 0) { - return false; - } - - // is the only currency left - if ($repository->get()->count() === 1) { - return false; - } - - // is the default currency for the user or the system - $defaultCode = Preferences::get('currencyPreference', config('firefly.default_currency', 'EUR'))->data; - if ($currency->code === $defaultCode) { - return false; - } - - // is the default currency for the system - $defaultSystemCode = config('firefly.default_currency', 'EUR'); - if ($currency->code === $defaultSystemCode) { - return false; - } - - // can be deleted - return true; - } - } diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index f8c20bc747..823c82e928 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -17,15 +17,16 @@ namespace FireflyIII\Http\Controllers; use Carbon\Carbon; use ExpandedForm; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Export\Processor; +use FireflyIII\Export\ProcessorInterface; use FireflyIII\Http\Requests\ExportFormRequest; use FireflyIII\Models\AccountType; use FireflyIII\Models\ExportJob; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\ExportJob\ExportJobRepositoryInterface; use FireflyIII\Repositories\ExportJob\ExportJobRepositoryInterface as EJRI; +use Illuminate\Http\Response as LaravelResponse; use Preferences; use Response; -use Storage; use View; /** @@ -41,32 +42,41 @@ class ExportController extends Controller public function __construct() { parent::__construct(); - View::share('mainTitleIcon', 'fa-file-archive-o'); - View::share('title', trans('firefly.export_data')); + + + $this->middleware( + function ($request, $next) { + View::share('mainTitleIcon', 'fa-file-archive-o'); + View::share('title', trans('firefly.export_data')); + + return $next($request); + } + ); } /** * @param ExportJob $job * - * @return mixed + * @return \Symfony\Component\HttpFoundation\Response|\Illuminate\Contracts\Routing\ResponseFactory * @throws FireflyException */ - public function download(ExportJob $job) + public function download(ExportJobRepositoryInterface $repository, ExportJob $job) { - $disk = Storage::disk('export'); $file = $job->key . '.zip'; $date = date('Y-m-d \a\t H-i-s'); $name = 'Export job on ' . $date . '.zip'; $quoted = sprintf('"%s"', addcslashes($name, '"\\')); - if (!$disk->exists($file)) { + if (!$repository->exists($job)) { throw new FireflyException('Against all expectations, zip file "' . $file . '" does not exist.'); } + $content = $repository->getContent($job); $job->change('export_downloaded'); - - return response($disk->get($file), 200) + /** @var LaravelResponse $response */ + $response = response($content, 200); + $response ->header('Content-Description', 'File Transfer') ->header('Content-Type', 'application/octet-stream') ->header('Content-Disposition', 'attachment; filename=' . $quoted) @@ -75,7 +85,9 @@ class ExportController extends Controller ->header('Expires', '0') ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') ->header('Pragma', 'public') - ->header('Content-Length', $disk->size($file)); + ->header('Content-Length', strlen($content)); + + return $response; } @@ -120,12 +132,10 @@ class ExportController extends Controller * @param AccountRepositoryInterface $repository * @param EJRI $jobs * - * @return string - * + * @return \Illuminate\Http\JsonResponse */ public function postIndex(ExportFormRequest $request, AccountRepositoryInterface $repository, EJRI $jobs) { - set_time_limit(0); $job = $jobs->findByKey($request->get('job')); $settings = [ 'accounts' => $repository->getAccountsById($request->get('accounts')), @@ -137,53 +147,55 @@ class ExportController extends Controller 'job' => $job, ]; - $job->change('export_status_make_exporter'); - $processor = new Processor($settings); + $jobs->changeStatus($job, 'export_status_make_exporter'); + + /** @var ProcessorInterface $processor */ + $processor = app(ProcessorInterface::class); + $processor->setSettings($settings); /* * Collect journals: */ - $job->change('export_status_collecting_journals'); + $jobs->changeStatus($job, 'export_status_collecting_journals'); $processor->collectJournals(); - $job->change('export_status_collected_journals'); + $jobs->changeStatus($job, 'export_status_collected_journals'); /* * Transform to exportable entries: */ - $job->change('export_status_converting_to_export_format'); + $jobs->changeStatus($job, 'export_status_converting_to_export_format'); $processor->convertJournals(); - $job->change('export_status_converted_to_export_format'); + $jobs->changeStatus($job, 'export_status_converted_to_export_format'); /* * Transform to (temporary) file: */ - $job->change('export_status_creating_journal_file'); + $jobs->changeStatus($job, 'export_status_creating_journal_file'); $processor->exportJournals(); - $job->change('export_status_created_journal_file'); + $jobs->changeStatus($job, 'export_status_created_journal_file'); /* * Collect attachments, if applicable. */ if ($settings['includeAttachments']) { - $job->change('export_status_collecting_attachments'); + $jobs->changeStatus($job, 'export_status_collecting_attachments'); $processor->collectAttachments(); - $job->change('export_status_collected_attachments'); + $jobs->changeStatus($job, 'export_status_collected_attachments'); } /* * Collect old uploads */ if ($settings['includeOldUploads']) { - $job->change('export_status_collecting_old_uploads'); + $jobs->changeStatus($job, 'export_status_collecting_old_uploads'); $processor->collectOldUploads(); - $job->change('export_status_collected_old_uploads'); + $jobs->changeStatus($job, 'export_status_collected_old_uploads'); } /* * Create ZIP file: */ - $job->change('export_status_creating_zip_file'); + $jobs->changeStatus($job, 'export_status_creating_zip_file'); $processor->createZipFile(); - $job->change('export_status_created_zip_file'); - - $job->change('export_status_finished'); + $jobs->changeStatus($job, 'export_status_created_zip_file'); + $jobs->changeStatus($job, 'export_status_finished'); return Response::json('ok'); } diff --git a/app/Http/Controllers/HelpController.php b/app/Http/Controllers/HelpController.php index 4b56c6e89e..24d2d75254 100644 --- a/app/Http/Controllers/HelpController.php +++ b/app/Http/Controllers/HelpController.php @@ -53,13 +53,30 @@ class HelpController extends Controller if ($help->inCache($route, $language)) { $content = $help->getFromCache($route, $language); - Log::debug('Help text was in cache.'); + Log::debug(sprintf('Help text %s was in cache.', $language)); return Response::json($content); } $content = $help->getFromGithub($language, $route); + // get backup language content (try English): + if (strlen($content) === 0) { + $language = 'en_US'; + if ($help->inCache($route, $language)) { + Log::debug(sprintf('Help text %s was in cache.', $language)); + $content = $help->getFromCache($route, $language); + } + if (!$help->inCache($route, $language)) { + $content = $help->getFromGithub($language, $route); + $content = '

' . strval(trans('firefly.help_may_not_be_your_language')) . '

' . $content; + } + } + + if (strlen($content) === 0) { + $content = '

' . strval(trans('firefly.route_has_no_help')) . '

'; + } + $help->putInCache($route, $language, $content); return Response::json($content); diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 08a1153e5d..9dbce2de32 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -15,18 +15,17 @@ namespace FireflyIII\Http\Controllers; use Artisan; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Models\AccountType; -use FireflyIII\Models\Tag; use FireflyIII\Repositories\Account\AccountRepositoryInterface as ARI; -use FireflyIII\Repositories\Account\AccountTaskerInterface; -use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; use Illuminate\Http\Request; use Illuminate\Support\Collection; use Log; use Preferences; use Route; use Session; - +use View; /** * Class HomeController @@ -41,6 +40,8 @@ class HomeController extends Controller public function __construct() { parent::__construct(); + View::share('title', 'Firefly III'); + View::share('mainTitleIcon', 'fa-fire'); } /** @@ -83,44 +84,26 @@ class HomeController extends Controller } /** - * @param TagRepositoryInterface $repository + * @param Request $request * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function flush(TagRepositoryInterface $repository) + public function flush(Request $request) { - Preferences::mark(); - - // get all tags. - // update all counts: - $tags = $repository->get(); - - /** @var Tag $tag */ - foreach ($tags as $tag) { - foreach ($tag->transactionJournals()->get() as $journal) { - $count = $journal->tags()->count(); - $journal->tag_count = $count; - $journal->save(); - } - } - - - Session::clear(); + $request->session()->forget(['start', 'end','_previous', 'viewRange', 'range', 'is_custom_range']); Artisan::call('cache:clear'); return redirect(route('index')); } /** - * @param ARI $repository - * @param AccountTaskerInterface $tasker + * @param ARI $repository * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View */ - public function index(ARI $repository, AccountTaskerInterface $tasker) + public function index(ARI $repository) { - $types = config('firefly.accountTypesByIdentifier.asset'); $count = $repository->count($types); @@ -128,11 +111,9 @@ class HomeController extends Controller return redirect(route('new-user.index')); } - $title = 'Firefly'; - $subTitle = trans('firefly.welcomeBack'); - $mainTitleIcon = 'fa-fire'; - $transactions = []; - $frontPage = Preferences::get( + $subTitle = trans('firefly.welcomeBack'); + $transactions = []; + $frontPage = Preferences::get( 'frontPageAccounts', $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])->pluck('id')->toArray() ); /** @var Carbon $start */ @@ -143,52 +124,23 @@ class HomeController extends Controller $accounts = $repository->getAccountsById($frontPage->data); $showDepositsFrontpage = Preferences::get('showDepositsFrontpage', false)->data; - foreach ($accounts as $account) { - $set = $tasker->getJournalsInPeriod(new Collection([$account]), [], $start, $end); - $set = $set->splice(0, 10); + // zero bills? Hide some elements from view. + /** @var BillRepositoryInterface $billRepository */ + $billRepository = app(BillRepositoryInterface::class); + $billCount = $billRepository->getBills()->count(); - if (count($set) > 0) { - $transactions[] = [$set, $account]; - } + foreach ($accounts as $account) { + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->setLimit(10)->setPage(1); + $set = $collector->getJournals(); + $transactions[] = [$set, $account]; } return view( - 'index', compact('count', 'showTour', 'title', 'subTitle', 'mainTitleIcon', 'transactions', 'showDepositsFrontpage') + 'index', compact('count', 'showTour', 'title', 'subTitle', 'mainTitleIcon', 'transactions', 'showDepositsFrontpage', 'billCount') ); } - /** - * Display a list of named routes. Excludes some that cannot be "shown". This method - * is used to generate help files (down the road). - */ - public function routes() - { - // these routes are not relevant for the help pages: - $ignore = ['login', 'registe', 'logout', 'two-fac', 'lost-two', 'confirm', 'resend', 'do_confirm', 'testFla', 'json.', 'piggy-banks.add', - 'piggy-banks.remove', 'preferences.', 'rules.rule.up', 'rules.rule.down', 'rules.rule-group.up', 'rules.rule-group.down', 'popup.report', - 'admin.users.domains.block-','import.json','help.' - ]; - $routes = Route::getRoutes(); - - echo '
';
-
-        /** @var \Illuminate\Routing\Route $route */
-        foreach ($routes as $route) {
-            $name    = $route->getName();
-            $methods = $route->getMethods();
-
-            if (!is_null($name) && strlen($name) > 0 && in_array('GET', $methods) && !$this->startsWithAny($ignore, $name)) {
-                echo sprintf('touch %s.md', $name)."\n";
-
-            }
-        }
-        echo '
'; - - echo '
'; - - return ' '; - } - /** * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ @@ -202,24 +154,4 @@ class HomeController extends Controller return redirect(route('home')); } - /** - * @param array $array - * @param string $needle - * - * @return bool - */ - private - function startsWithAny( - array $array, string $needle - ): bool - { - foreach ($array as $entry) { - if ((substr($needle, 0, strlen($entry)) === $entry)) { - return true; - } - } - - return false; - } - } diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index 05047e0e81..ef812a8a1b 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -15,14 +15,16 @@ namespace FireflyIII\Http\Controllers; use Crypt; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Requests\ImportUploadRequest; -use FireflyIII\Import\ImportProcedure; +use FireflyIII\Import\ImportProcedureInterface; use FireflyIII\Import\Setup\SetupInterface; use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use Illuminate\Http\Request; +use Illuminate\Http\Response as LaravelResponse; use Log; use Response; +use Session; use SplFileObject; use Storage; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -41,8 +43,15 @@ class ImportController extends Controller public function __construct() { parent::__construct(); - View::share('mainTitleIcon', 'fa-archive'); - View::share('title', trans('firefly.import_data_full')); + + $this->middleware( + function ($request, $next) { + View::share('mainTitleIcon', 'fa-archive'); + View::share('title', trans('firefly.import_data_full')); + + return $next($request); + } + ); } /** @@ -112,15 +121,18 @@ class ImportController extends Controller $result = json_encode($config, JSON_PRETTY_PRINT); $name = sprintf('"%s"', addcslashes('import-configuration-' . date('Y-m-d') . '.json', '"\\')); - return response($result, 200) - ->header('Content-disposition', 'attachment; filename=' . $name) - ->header('Content-Type', 'application/json') - ->header('Content-Description', 'File Transfer') - ->header('Connection', 'Keep-Alive') - ->header('Expires', '0') - ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - ->header('Pragma', 'public') - ->header('Content-Length', strlen($result)); + /** @var LaravelResponse $response */ + $response = response($result, 200); + $response->header('Content-disposition', 'attachment; filename=' . $name) + ->header('Content-Type', 'application/json') + ->header('Content-Description', 'File Transfer') + ->header('Connection', 'Keep-Alive') + ->header('Expires', '0') + ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->header('Pragma', 'public') + ->header('Content-Length', strlen($result)); + + return $response; } @@ -138,10 +150,13 @@ class ImportController extends Controller 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')); + return view('import.finished', compact('job', 'subTitle', 'subTitleIcon', 'tagId')); } /** @@ -305,13 +320,14 @@ class ImportController extends Controller } /** - * @param ImportJob $job + * @param ImportProcedureInterface $importProcedure + * @param ImportJob $job */ - public function start(ImportJob $job) + public function start(ImportProcedureInterface $importProcedure, ImportJob $job) { set_time_limit(0); if ($job->status == 'settings_complete') { - ImportProcedure::runImport($job); + $importProcedure->runImport($job); } } @@ -323,7 +339,7 @@ class ImportController extends Controller * @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); @@ -357,14 +373,33 @@ class ImportController extends Controller $content = $uploaded->fread($uploaded->getSize()); $contentEncrypted = Crypt::encrypt($content); $disk = Storage::disk('upload'); - $disk->put($newName, $contentEncrypted); - Log::debug('Uploaded file', ['name' => $upload->getClientOriginalName(), 'size' => $upload->getSize(), 'mime' => $upload->getClientMimeType()]); + // user is demo user, replace upload with prepared file. + if (auth()->user()->hasRole('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.'); - // store configuration file's content into the job's configuration - // thing. - // otherwise, leave it empty. - if ($request->files->has('configuration_file')) { + // also set up prepared configuration. + $configuration = json_decode($stubsDisk->get('demo-configuration.json'), true); + $job->configuration = $configuration; + $job->save(); + Log::debug('Set configuration for demo user', $configuration); + + // also flash info + Session::flash('info', trans('demo.import-configure-security')); + } + if (!auth()->user()->hasRole('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( @@ -383,6 +418,9 @@ class ImportController extends Controller } } + // if user is demo user, replace config with prepared config: + + return redirect(route('import.configure', [$job->key])); } diff --git a/app/Http/Controllers/JavascriptController.php b/app/Http/Controllers/JavascriptController.php new file mode 100644 index 0000000000..6e63e57a43 --- /dev/null +++ b/app/Http/Controllers/JavascriptController.php @@ -0,0 +1,134 @@ +getDateRangePicker(); + $start = Session::get('start'); + $end = Session::get('end'); + $linkTitle = sprintf('%s - %s', $start->formatLocalized($this->monthAndDayFormat), $end->formatLocalized($this->monthAndDayFormat)); + $firstDate = session('first')->format('Y-m-d'); + $localeconv = localeconv(); + $accounting = Amount::getJsConfig($localeconv); + $localeconv = localeconv(); + $defaultCurrency = Amount::getDefaultCurrency(); + $localeconv['frac_digits'] = $defaultCurrency->decimal_places; + $pref = Preferences::get('language', config('firefly.default_language', 'en_US')); + $lang = $pref->data; + $data = [ + 'picker' => $picker, + 'linkTitle' => $linkTitle, + 'firstDate' => $firstDate, + 'currencyCode' => Amount::getCurrencyCode(), + 'currencySymbol' => Amount::getCurrencySymbol(), + 'accounting' => $accounting, + 'localeconv' => $localeconv, + 'language' => $lang, + ]; + $request->session()->keep(['two-factor-secret']); + + return response() + ->view('javascript.variables', $data, 200) + ->header('Content-Type', 'text/javascript'); + } + + /** + * @return array + * @throws FireflyException + */ + private function getDateRangePicker(): array + { + $viewRange = Preferences::get('viewRange', '1M')->data; + $start = Session::get('start'); + $end = Session::get('end'); + + $prevStart = clone $start; + $prevEnd = clone $start; + $nextStart = clone $end; + $nextEnd = clone $end; + if ($viewRange === 'custom') { + $days = $start->diffInDays($end); + $prevStart->subDays($days); + $nextEnd->addDays($days); + unset($days); + } + + if ($viewRange !== 'custom') { + $prevStart = Navigation::subtractPeriod($start, $viewRange);// subtract for previous period + $prevEnd = Navigation::endOfPeriod($prevStart, $viewRange); + $nextStart = Navigation::addPeriod($start, $viewRange, 0); // add for previous period + $nextEnd = Navigation::endOfPeriod($nextStart, $viewRange); + } + + $ranges = []; + $ranges['current'] = [$start->format('Y-m-d'), $end->format('Y-m-d')]; + $ranges['previous'] = [$prevStart->format('Y-m-d'), $prevEnd->format('Y-m-d')]; + $ranges['next'] = [$nextStart->format('Y-m-d'), $nextEnd->format('Y-m-d')]; + + switch ($viewRange) { + default: + throw new FireflyException('The date picker does not yet support "' . $viewRange . '".'); + case '1D': + case 'custom': + $format = (string)trans('config.month_and_day'); + break; + case '3M': + $format = (string)trans('config.quarter_in_year'); + break; + case '6M': + $format = (string)trans('config.half_year'); + break; + case '1Y': + $format = (string)trans('config.year'); + break; + case '1M': + $format = (string)trans('config.month'); + break; + case '1W': + $format = (string)trans('config.week_in_year'); + break; + } + + $current = $start->formatLocalized($format); + $next = $nextStart->formatLocalized($format); + $prev = $prevStart->formatLocalized($format); + + return [ + 'start' => $start->format('Y-m-d'), + 'end' => $end->format('Y-m-d'), + 'current' => $current, + 'previous' => $prev, + 'next' => $next, + 'ranges' => $ranges, + ]; + } + +} diff --git a/app/Http/Controllers/JsonController.php b/app/Http/Controllers/JsonController.php index 5eea913966..4d95c51fc9 100644 --- a/app/Http/Controllers/JsonController.php +++ b/app/Http/Controllers/JsonController.php @@ -15,15 +15,17 @@ namespace FireflyIII\Http\Controllers; use Amount; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; -use FireflyIII\Repositories\Category\CategoryRepositoryInterface as CRI; -use FireflyIII\Repositories\Journal\JournalTaskerInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Support\CacheProperties; -use Input; +use Illuminate\Http\Request; use Preferences; use Response; @@ -43,11 +45,13 @@ class JsonController extends Controller } /** + * @param Request $request + * * @return \Illuminate\Http\JsonResponse */ - public function action() + public function action(Request $request) { - $count = intval(Input::get('count')) > 0 ? intval(Input::get('count')) : 1; + $count = intval($request->get('count')) > 0 ? intval($request->get('count')) : 1; $keys = array_keys(config('firefly.rule-actions')); $actions = []; foreach ($keys as $key) { @@ -59,6 +63,43 @@ class JsonController extends Controller return Response::json(['html' => $view]); } + /** + * Returns a JSON list of all accounts. + * + * @param AccountRepositoryInterface $repository + * + * @return \Illuminate\Http\JsonResponse + * + */ + public function allAccounts(AccountRepositoryInterface $repository) + { + $return = array_unique( + $repository->getAccountsByType( + [AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET] + )->pluck('name')->toArray() + ); + sort($return); + + return Response::json($return); + + } + + /** + * @param JournalCollectorInterface $collector + * + * @return \Illuminate\Http\JsonResponse + */ + public function allTransactionJournals(JournalCollectorInterface $collector) + { + $collector->setLimit(100)->setPage(1); + $return = array_unique($collector->getJournals()->pluck('description')->toArray()); + sort($return); + + return Response::json($return); + + + } + /** * @param BillRepositoryInterface $repository * @@ -102,7 +143,6 @@ class JsonController extends Controller * * @return \Illuminate\Http\JsonResponse * - * @internal param ARI $accountRepository */ public function boxIn(AccountTaskerInterface $accountTasker, AccountRepositoryInterface $repository) { @@ -157,19 +197,29 @@ class JsonController extends Controller } /** - * Returns a list of categories. - * - * @param CRI $repository + * @param BudgetRepositoryInterface $repository * * @return \Illuminate\Http\JsonResponse */ - public function categories(CRI $repository) + public function budgets(BudgetRepositoryInterface $repository) { - $list = $repository->getCategories(); - $return = []; - foreach ($list as $entry) { - $return[] = $entry->name; - } + $return = array_unique($repository->getBudgets()->pluck('name')->toArray()); + sort($return); + + return Response::json($return); + } + + /** + * Returns a list of categories. + * + * @param CategoryRepositoryInterface $repository + * + * @return \Illuminate\Http\JsonResponse + */ + public function categories(CategoryRepositoryInterface $repository) + { + $return = array_unique($repository->getCategories()->pluck('name')->toArray()); + sort($return); return Response::json($return); } @@ -194,14 +244,10 @@ class JsonController extends Controller */ public function expenseAccounts(AccountRepositoryInterface $repository) { - $list = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]); - $return = []; - foreach ($list as $entry) { - $return[] = $entry->name; - } + $return = array_unique($repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY])->pluck('name')->toArray()); + sort($return); return Response::json($return); - } /** @@ -212,14 +258,10 @@ class JsonController extends Controller */ public function revenueAccounts(AccountRepositoryInterface $repository) { - $list = $repository->getAccountsByType([AccountType::REVENUE]); - $return = []; - foreach ($list as $entry) { - $return[] = $entry->name; - } + $return = array_unique($repository->getAccountsByType([AccountType::REVENUE])->pluck('name')->toArray()); + sort($return); return Response::json($return); - } /** @@ -231,11 +273,8 @@ class JsonController extends Controller */ public function tags(TagRepositoryInterface $tagRepository) { - $list = $tagRepository->get(); - $return = []; - foreach ($list as $entry) { - $return[] = $entry->tag; - } + $return = array_unique($tagRepository->get()->pluck('tag')->toArray()); + sort($return); return Response::json($return); @@ -270,35 +309,44 @@ class JsonController extends Controller } /** - * @param JournalTaskerInterface $tasker - * @param $what + * @param JournalCollectorInterface $collector + * @param string $what * - * @return \Symfony\Component\HttpFoundation\Response + * @return \Illuminate\Http\JsonResponse */ - public function transactionJournals(JournalTaskerInterface $tasker, $what) + public function transactionJournals(JournalCollectorInterface $collector, string $what) { - $descriptions = []; - $type = config('firefly.transactionTypesByWhat.' . $what); - $types = [$type]; - $journals = $tasker->getJournals($types, 1, 50); - foreach ($journals as $j) { - $descriptions[] = $j->description; - } + $type = config('firefly.transactionTypesByWhat.' . $what); + $types = [$type]; - $descriptions = array_unique($descriptions); - sort($descriptions); + $collector->setTypes($types)->setLimit(100)->setPage(1); + $return = array_unique($collector->getJournals()->pluck('description')->toArray()); + sort($return); - return Response::json($descriptions); + return Response::json($return); } /** + * + */ + public function transactionTypes(JournalRepositoryInterface $repository) + { + $return = array_unique($repository->getTransactionTypes()->pluck('type')->toArray()); + sort($return); + + return Response::json($return); + } + + /** + * @param Request $request + * * @return \Illuminate\Http\JsonResponse */ - public function trigger() + public function trigger(Request $request) { - $count = intval(Input::get('count')) > 0 ? intval(Input::get('count')) : 1; + $count = intval($request->get('count')) > 0 ? intval($request->get('count')) : 1; $keys = array_keys(config('firefly.rule-triggers')); $triggers = []; foreach ($keys as $key) { diff --git a/app/Http/Controllers/NewUserController.php b/app/Http/Controllers/NewUserController.php index 1fe99c6918..12a9c396a9 100644 --- a/app/Http/Controllers/NewUserController.php +++ b/app/Http/Controllers/NewUserController.php @@ -32,6 +32,13 @@ class NewUserController extends Controller public function __construct() { parent::__construct(); + + $this->middleware( + function ($request, $next) { + + return $next($request); + } + ); } @@ -71,14 +78,16 @@ class NewUserController extends Controller $this->createAssetAccount($request, $repository); // create savings account - if (strlen($request->get('savings_balance')) > 0) { + $savingBalance = strval($request->get('savings_balance')) === '' ? '0' : strval($request->get('savings_balance')); + if (bccomp($savingBalance, '0') !== 0) { $this->createSavingsAccount($request, $repository); $count++; } // create credit card. - if (strlen($request->get('credit_card_limit')) > 0) { + $limit = strval($request->get('credit_card_limit')) === '' ? '0' : strval($request->get('credit_card_limit')); + if (bccomp($limit, '0') !== 0) { $this->storeCreditCard($request, $repository); $count++; } @@ -108,7 +117,7 @@ class NewUserController extends Controller 'virtualBalance' => 0, 'active' => true, 'accountRole' => 'defaultAsset', - 'openingBalance' => round($request->input('bank_balance'), 2), + 'openingBalance' => round($request->input('bank_balance'), 12), 'openingBalanceDate' => new Carbon, 'openingBalanceCurrency' => intval($request->input('amount_currency_id_bank_balance')), ]; @@ -133,7 +142,7 @@ class NewUserController extends Controller 'virtualBalance' => 0, 'active' => true, 'accountRole' => 'savingAsset', - 'openingBalance' => round($request->input('savings_balance'), 2), + 'openingBalance' => round($request->input('savings_balance'), 12), 'openingBalanceDate' => new Carbon, 'openingBalanceCurrency' => intval($request->input('amount_currency_id_savings_balance')), ]; @@ -154,7 +163,7 @@ class NewUserController extends Controller 'name' => 'Credit card', 'iban' => null, 'accountType' => 'asset', - 'virtualBalance' => round($request->get('credit_card_limit'), 2), + 'virtualBalance' => round($request->get('credit_card_limit'), 12), 'active' => true, 'accountRole' => 'ccAsset', 'openingBalance' => null, diff --git a/app/Http/Controllers/PiggyBankController.php b/app/Http/Controllers/PiggyBankController.php index 70c3ae8487..b74905c682 100644 --- a/app/Http/Controllers/PiggyBankController.php +++ b/app/Http/Controllers/PiggyBankController.php @@ -20,13 +20,13 @@ use FireflyIII\Models\AccountType; use FireflyIII\Models\PiggyBank; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; +use Illuminate\Http\Request; use Illuminate\Support\Collection; -use Input; use Log; use Preferences; +use Response; use Session; use Steam; -use URL; use View; /** @@ -45,8 +45,16 @@ class PiggyBankController extends Controller public function __construct() { parent::__construct(); - View::share('title', trans('firefly.piggyBanks')); - View::share('mainTitleIcon', 'fa-sort-amount-asc'); + + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.piggyBanks')); + View::share('mainTitleIcon', 'fa-sort-amount-asc'); + + return $next($request); + } + ); } /** @@ -99,9 +107,15 @@ class PiggyBankController extends Controller $subTitle = trans('firefly.new_piggy_bank'); $subTitleIcon = 'fa-plus'; + if (count($accounts) === 0) { + Session::flash('error', strval(trans('firefly.need_at_least_one_account'))); + + return redirect(route('new-user.index')); + } + // put previous url in session if not redirect from store (not "create another"). if (session('piggy-banks.create.fromStore') !== true) { - Session::put('piggy-banks.create.url', URL::previous()); + $this->rememberPreviousUri('piggy-banks.create.uri'); } Session::forget('piggy-banks.create.fromStore'); Session::flash('gaEventCategory', 'piggy-banks'); @@ -120,7 +134,7 @@ class PiggyBankController extends Controller $subTitle = trans('firefly.delete_piggy_bank', ['name' => $piggyBank->name]); // put previous url in session - Session::put('piggy-banks.delete.url', URL::previous()); + $this->rememberPreviousUri('piggy-banks.delete.uri'); Session::flash('gaEventCategory', 'piggy-banks'); Session::flash('gaEventAction', 'delete'); @@ -135,13 +149,11 @@ class PiggyBankController extends Controller */ public function destroy(PiggyBankRepositoryInterface $repository, PiggyBank $piggyBank) { - - Session::flash('success', strval(trans('firefly.deleted_piggy_bank', ['name' => e($piggyBank->name)]))); Preferences::mark(); $repository->destroy($piggyBank); - return redirect(session('piggy-banks.delete.url')); + return redirect($this->getPreviousUri('piggy-banks.delete.uri')); } /** @@ -177,7 +189,7 @@ class PiggyBankController extends Controller // put previous url in session if not redirect from store (not "return_to_edit"). if (session('piggy-banks.edit.fromUpdate') !== true) { - Session::put('piggy-banks.edit.url', URL::previous()); + $this->rememberPreviousUri('piggy-banks.edit.uri'); } Session::forget('piggy-banks.edit.fromUpdate'); @@ -199,7 +211,7 @@ class PiggyBankController extends Controller $accounts = []; /** @var PiggyBank $piggyBank */ foreach ($piggyBanks as $piggyBank) { - $piggyBank->savedSoFar = round($piggyBank->currentRelevantRep()->currentamount, 2); + $piggyBank->savedSoFar = $piggyBank->currentRelevantRep()->currentamount; $piggyBank->percentage = $piggyBank->savedSoFar != 0 ? intval($piggyBank->savedSoFar / $piggyBank->targetamount * 100) : 0; $piggyBank->leftToSave = bcsub($piggyBank->targetamount, strval($piggyBank->savedSoFar)); $piggyBank->percentage = $piggyBank->percentage > 100 ? 100 : $piggyBank->percentage; @@ -214,7 +226,7 @@ class PiggyBankController extends Controller 'balance' => Steam::balanceIgnoreVirtual($account, $end), 'leftForPiggyBanks' => $piggyBank->leftOnAccount($end), 'sumOfSaved' => strval($piggyBank->savedSoFar), - 'sumOfTargets' => strval(round($piggyBank->targetamount, 2)), + 'sumOfTargets' => $piggyBank->targetamount, 'leftToSave' => $piggyBank->leftToSave, ]; } else { @@ -228,11 +240,14 @@ class PiggyBankController extends Controller } /** + * @param Request $request * @param PiggyBankRepositoryInterface $repository + * + * @return \Illuminate\Http\JsonResponse */ - public function order(PiggyBankRepositoryInterface $repository) + public function order(Request $request, PiggyBankRepositoryInterface $repository) { - $data = Input::get('order'); + $data = $request->get('order'); // set all users piggy banks to zero: $repository->reset(); @@ -243,25 +258,29 @@ class PiggyBankController extends Controller $repository->setOrder(intval($id), ($order + 1)); } } + + return Response::json(['result' => 'ok']); } /** + * @param Request $request * @param PiggyBankRepositoryInterface $repository * @param PiggyBank $piggyBank * * @return \Illuminate\Http\RedirectResponse */ - public function postAdd(PiggyBankRepositoryInterface $repository, PiggyBank $piggyBank) + public function postAdd(Request $request, PiggyBankRepositoryInterface $repository, PiggyBank $piggyBank) { - $amount = strval(round(Input::get('amount'), 2)); + $amount = $request->get('amount'); + Log::debug(sprintf('Found amount is %s', $amount)); /** @var Carbon $date */ $date = session('end', Carbon::now()->endOfMonth()); $leftOnAccount = $piggyBank->leftOnAccount($date); $savedSoFar = strval($piggyBank->currentRelevantRep()->currentamount); $leftToSave = bcsub($piggyBank->targetamount, $savedSoFar); - $maxAmount = round(min($leftOnAccount, $leftToSave), 2); + $maxAmount = strval(min(round($leftOnAccount, 12), round($leftToSave, 12))); - if ($amount <= $maxAmount) { + if (bccomp($amount, $maxAmount) <= 0) { $repetition = $piggyBank->currentRelevantRep(); $currentAmount = $repetition->currentamount ?? '0'; $repetition->currentamount = bcadd($currentAmount, $amount); @@ -285,18 +304,19 @@ class PiggyBankController extends Controller } /** + * @param Request $request * @param PiggyBankRepositoryInterface $repository * @param PiggyBank $piggyBank * * @return \Illuminate\Http\RedirectResponse */ - public function postRemove(PiggyBankRepositoryInterface $repository, PiggyBank $piggyBank) + public function postRemove(Request $request, PiggyBankRepositoryInterface $repository, PiggyBank $piggyBank) { - $amount = strval(round(Input::get('amount'), 2)); + $amount = strval(round($request->get('amount'), 12)); $savedSoFar = $piggyBank->currentRelevantRep()->currentamount; - if ($amount <= $savedSoFar) { + if (bccomp($amount, $savedSoFar) <= 0) { $repetition = $piggyBank->currentRelevantRep(); $repetition->currentamount = bcsub($repetition->currentamount, $amount); $repetition->save(); @@ -364,21 +384,19 @@ class PiggyBankController extends Controller */ public function store(PiggyBankFormRequest $request, PiggyBankRepositoryInterface $repository) { - $data = $request->getPiggyBankData(); + $data = $request->getPiggyBankData(); $piggyBank = $repository->store($data); Session::flash('success', strval(trans('firefly.stored_piggy_bank', ['name' => e($piggyBank->name)]))); Preferences::mark(); - if (intval(Input::get('create_another')) === 1) { + if (intval($request->get('create_another')) === 1) { Session::put('piggy-banks.create.fromStore', true); return redirect(route('piggy-banks.create'))->withInput(); } - - // redirect to previous URL. - return redirect(session('piggy-banks.create.url')); + return redirect($this->getPreviousUri('piggy-banks.edit.uri')); } /** @@ -386,28 +404,22 @@ class PiggyBankController extends Controller * @param PiggyBankFormRequest $request * @param PiggyBank $piggyBank * - * @return $this + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ public function update(PiggyBankRepositoryInterface $repository, PiggyBankFormRequest $request, PiggyBank $piggyBank) { - $data = $request->getPiggyBankData(); + $data = $request->getPiggyBankData(); $piggyBank = $repository->update($piggyBank, $data); Session::flash('success', strval(trans('firefly.updated_piggy_bank', ['name' => e($piggyBank->name)]))); Preferences::mark(); - if (intval(Input::get('return_to_edit')) === 1) { + if (intval($request->get('return_to_edit')) === 1) { Session::put('piggy-banks.edit.fromUpdate', true); return redirect(route('piggy-banks.edit', [$piggyBank->id])); } - - // redirect to previous URL. - return redirect(session('piggy-banks.edit.url')); - - + return redirect($this->getPreviousUri('piggy-banks.edit.uri')); } - - } diff --git a/app/Http/Controllers/Popup/ReportController.php b/app/Http/Controllers/Popup/ReportController.php index 0b4df645ba..a2f3505923 100644 --- a/app/Http/Controllers/Popup/ReportController.php +++ b/app/Http/Controllers/Popup/ReportController.php @@ -17,12 +17,12 @@ namespace FireflyIII\Http\Controllers\Popup; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collection\BalanceLine; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Support\Binder\AccountList; @@ -46,7 +46,7 @@ class ReportController extends Controller * @return \Illuminate\Http\JsonResponse * @throws FireflyException */ - public function info(Request $request) + public function general(Request $request) { $attributes = $request->get('attributes') ?? []; $attributes = $this->parseAttributes($attributes); @@ -98,24 +98,44 @@ class ReportController extends Controller $repository = app(AccountRepositoryInterface::class); $account = $repository->find(intval($attributes['accountId'])); + $types = [TransactionType::WITHDRAWAL]; switch (true) { case ($role === BalanceLine::ROLE_DEFAULTROLE && !is_null($budget->id)): - $journals = $budgetRepository->journalsInPeriod( - new Collection([$budget]), new Collection([$account]), $attributes['startDate'], $attributes['endDate'] - ); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector + ->setAccounts(new Collection([$account])) + ->setRange($attributes['startDate'], $attributes['endDate']) + ->setBudget($budget); + $journals = $collector->getJournals(); + break; case ($role === BalanceLine::ROLE_DEFAULTROLE && is_null($budget->id)): $budget->name = strval(trans('firefly.no_budget')); - $journals = $budgetRepository->journalsInPeriodWithoutBudget($attributes['accounts'], $attributes['startDate'], $attributes['endDate']); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector + ->setAccounts(new Collection([$account])) + ->setTypes($types) + ->setRange($attributes['startDate'], $attributes['endDate']) + ->withoutBudget(); + $journals = $collector->getJournals(); break; case ($role === BalanceLine::ROLE_DIFFROLE): - // journals no budget, not corrected by a tag. - $journals = $budgetRepository->journalsInPeriodWithoutBudget($attributes['accounts'], $attributes['startDate'], $attributes['endDate']); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector + ->setAccounts(new Collection([$account])) + ->setTypes($types) + ->setRange($attributes['startDate'], $attributes['endDate']) + ->withoutBudget(); + $journals = $collector->getJournals(); + $budget->name = strval(trans('firefly.leftUnbalanced')); $journals = $journals->filter( - function (TransactionJournal $journal) { - $tags = $journal->tags()->where('tagMode', 'balancingAct')->count(); + function (Transaction $transaction) { + $tags = $transaction->transactionJournal->tags()->where('tagMode', 'balancingAct')->count(); if ($tags === 0) { return true; } @@ -148,14 +168,22 @@ class ReportController extends Controller /** @var BudgetRepositoryInterface $repository */ $repository = app(BudgetRepositoryInterface::class); $budget = $repository->find(intval($attributes['budgetId'])); - if (is_null($budget->id)) { - $journals = $repository->journalsInPeriodWithoutBudget($attributes['accounts'], $attributes['startDate'], $attributes['endDate']); - } else { - // get all expenses in budget in period: - $journals = $repository->journalsInPeriod(new Collection([$budget]), $attributes['accounts'], $attributes['startDate'], $attributes['endDate']); - } + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); - $view = view('popup.report.budget-spent-amount', compact('journals', 'budget'))->render(); + $collector + ->setAccounts($attributes['accounts']) + ->setRange($attributes['startDate'], $attributes['endDate']); + + if (is_null($budget->id)) { + $collector->setTypes([TransactionType::WITHDRAWAL])->withoutBudget(); + } + if (!is_null($budget->id)) { + // get all expenses in budget in period: + $collector->setBudget($budget); + } + $journals = $collector->getJournals(); + $view = view('popup.report.budget-spent-amount', compact('journals', 'budget'))->render(); return $view; } @@ -174,10 +202,14 @@ class ReportController extends Controller $repository = app(CategoryRepositoryInterface::class); $category = $repository->find(intval($attributes['categoryId'])); $types = [TransactionType::WITHDRAWAL, TransactionType::TRANSFER]; - $journals = $repository->journalsInPeriod( - new Collection([$category]), $attributes['accounts'], $types, $attributes['startDate'], $attributes['endDate'] - ); - $view = view('popup.report.category-entry', compact('journals', 'category'))->render(); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($attributes['accounts'])->setTypes($types) + ->setRange($attributes['startDate'], $attributes['endDate']) + ->setCategory($category); + $journals = $collector->getJournals(); // 7193 + + $view = view('popup.report.category-entry', compact('journals', 'category'))->render(); return $view; } @@ -192,14 +224,15 @@ class ReportController extends Controller */ private function expenseEntry(array $attributes): string { - /** @var AccountTaskerInterface $tasker */ - $tasker = app(AccountTaskerInterface::class); /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); - $account = $repository->find(intval($attributes['accountId'])); - $types = [TransactionType::WITHDRAWAL, TransactionType::TRANSFER]; - $journals = $tasker->getJournalsInPeriod(new Collection([$account]), $types, $attributes['startDate'], $attributes['endDate']); + $account = $repository->find(intval($attributes['accountId'])); + $types = [TransactionType::WITHDRAWAL, TransactionType::TRANSFER]; + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts(new Collection([$account]))->setRange($attributes['startDate'], $attributes['endDate'])->setTypes($types); + $journals = $collector->getJournals(); $report = $attributes['accounts']->pluck('id')->toArray(); // accounts used in this report // filter for transfers and withdrawals TO the given $account @@ -228,14 +261,15 @@ class ReportController extends Controller */ private function incomeEntry(array $attributes): string { - /** @var AccountTaskerInterface $tasker */ - $tasker = app(AccountTaskerInterface::class); /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); $account = $repository->find(intval($attributes['accountId'])); $types = [TransactionType::DEPOSIT, TransactionType::TRANSFER]; - $journals = $tasker->getJournalsInPeriod(new Collection([$account]), $types, $attributes['startDate'], $attributes['endDate']); - $report = $attributes['accounts']->pluck('id')->toArray(); // accounts used in this report + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts(new Collection([$account]))->setRange($attributes['startDate'], $attributes['endDate'])->setTypes($types); + $journals = $collector->getJournals(); + $report = $attributes['accounts']->pluck('id')->toArray(); // accounts used in this report // filter the set so the destinations outside of $attributes['accounts'] are not included. $journals = $journals->filter( diff --git a/app/Http/Controllers/PreferencesController.php b/app/Http/Controllers/PreferencesController.php index ae371c7e98..4373d61204 100644 --- a/app/Http/Controllers/PreferencesController.php +++ b/app/Http/Controllers/PreferencesController.php @@ -35,8 +35,16 @@ class PreferencesController extends Controller public function __construct() { parent::__construct(); - View::share('title', trans('firefly.preferences')); - View::share('mainTitleIcon', 'fa-gear'); + + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.preferences')); + View::share('mainTitleIcon', 'fa-gear'); + + return $next($request); + } + ); } /** @@ -48,9 +56,9 @@ class PreferencesController extends Controller { $domain = $this->getDomain(); /** @noinspection PhpMethodParametersCountMismatchInspection */ - $secret = $google2fa->generateSecretKey(16, auth()->user()->id); + $secret = $google2fa->generateSecretKey(32, auth()->user()->id); Session::flash('two-factor-secret', $secret); - $image = $google2fa->getQRCodeInline('Firefly III at ' . $domain, null, $secret, 150); + $image = $google2fa->getQRCodeInline('Firefly III at ' . $domain, auth()->user()->email, $secret, 150); return view('preferences.code', compact('image')); @@ -66,7 +74,7 @@ class PreferencesController extends Controller Session::flash('success', strval(trans('firefly.pref_two_factor_auth_disabled'))); Session::flash('info', strval(trans('firefly.pref_two_factor_auth_remove_it'))); - return redirect(route('preferences')); + return redirect(route('preferences.index')); } /** @@ -105,6 +113,7 @@ class PreferencesController extends Controller * @param TokenFormRequest $request * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @SuppressWarnings(PHPMD.UnusedFormalParameter) // it's unused but the class does some validation. */ public function postCode(TokenFormRequest $request) { @@ -114,7 +123,7 @@ class PreferencesController extends Controller Session::flash('success', strval(trans('firefly.saved_preferences'))); Preferences::mark(); - return redirect(route('preferences')); + return redirect(route('preferences.index')); } /** @@ -142,7 +151,7 @@ class PreferencesController extends Controller // custom fiscal year $customFiscalYear = intval($request->get('customFiscalYear')) === 1; - $fiscalYearStart = date('m-d', strtotime($request->get('fiscalYearStart'))); + $fiscalYearStart = date('m-d', strtotime(strval($request->get('fiscalYearStart')))); Preferences::set('customFiscalYear', $customFiscalYear); Preferences::set('fiscalYearStart', $fiscalYearStart); @@ -158,13 +167,17 @@ class PreferencesController extends Controller Preferences::set('transactionPageSize', 50); } - // two factor auth - $twoFactorAuthEnabled = intval($request->get('twoFactorAuthEnabled')); - $hasTwoFactorAuthSecret = !is_null(Preferences::get('twoFactorAuthSecret')); + $twoFactorAuthEnabled = false; + $hasTwoFactorAuthSecret = false; + if (!auth()->user()->hasRole('demo')) { + // two factor auth + $twoFactorAuthEnabled = intval($request->get('twoFactorAuthEnabled')); + $hasTwoFactorAuthSecret = !is_null(Preferences::get('twoFactorAuthSecret')); - // If we already have a secret, just set the two factor auth enabled to 1, and let the user continue with the existing secret. - if ($hasTwoFactorAuthSecret) { - Preferences::set('twoFactorAuthEnabled', $twoFactorAuthEnabled); + // If we already have a secret, just set the two factor auth enabled to 1, and let the user continue with the existing secret. + if ($hasTwoFactorAuthSecret) { + Preferences::set('twoFactorAuthEnabled', $twoFactorAuthEnabled); + } } // language: @@ -198,13 +211,13 @@ class PreferencesController extends Controller return redirect(route('preferences.code')); } - return redirect(route('preferences')); + return redirect(route('preferences.index')); } /** * @return string */ - private function getDomain() : string + private function getDomain(): string { $url = url()->to('/'); $parts = parse_url($url); diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 5d368c09f5..e2f7516dd8 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -13,12 +13,12 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers; -use FireflyIII\Events\DeletedUser; +use FireflyIII\Exceptions\ValidationException; use FireflyIII\Http\Requests\DeleteAccountFormRequest; use FireflyIII\Http\Requests\ProfileFormRequest; -use FireflyIII\User; +use FireflyIII\Repositories\User\UserRepositoryInterface; use Hash; -use Preferences; +use Log; use Session; use View; @@ -36,8 +36,15 @@ class ProfileController extends Controller { parent::__construct(); - View::share('title', trans('firefly.profile')); - View::share('mainTitleIcon', 'fa-user'); + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.profile')); + View::share('mainTitleIcon', 'fa-user'); + + return $next($request); + } + ); } /** @@ -45,9 +52,21 @@ class ProfileController extends Controller */ public function changePassword() { - return view('profile.change-password')->with('title', auth()->user()->email)->with('subTitle', trans('firefly.change_your_password'))->with( - 'mainTitleIcon', 'fa-user' - ); + if (intval(getenv('SANDSTORM')) === 1) { + return view('error')->with('message', strval(trans('firefly.sandstorm_not_available'))); + } + + if (auth()->user()->hasRole('demo')) { + Session::flash('info', strval(trans('firefly.cannot_change_demo'))); + + return redirect(route('profile.index')); + } + + $title = auth()->user()->email; + $subTitle = strval(trans('firefly.change_your_password')); + $subTitleIcon = 'fa-key'; + + return view('profile.change-password', compact('title', 'subTitle', 'subTitleIcon')); } /** @@ -55,9 +74,21 @@ class ProfileController extends Controller */ public function deleteAccount() { - return view('profile.delete-account')->with('title', auth()->user()->email)->with('subTitle', trans('firefly.delete_account'))->with( - 'mainTitleIcon', 'fa-user' - ); + if (intval(getenv('SANDSTORM')) === 1) { + return view('error')->with('message', strval(trans('firefly.sandstorm_not_available'))); + } + + if (auth()->user()->hasRole('demo')) { + Session::flash('info', strval(trans('firefly.cannot_delete_demo'))); + + return redirect(route('profile.index')); + } + + $title = auth()->user()->email; + $subTitle = strval(trans('firefly.delete_account')); + $subTitleIcon = 'fa-trash'; + + return view('profile.delete-account', compact('title', 'subTitle', 'subTitleIcon')); } /** @@ -73,91 +104,94 @@ class ProfileController extends Controller } /** - * @param ProfileFormRequest $request + * @param ProfileFormRequest $request + * @param UserRepositoryInterface $repository * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function postChangePassword(ProfileFormRequest $request) + public function postChangePassword(ProfileFormRequest $request, UserRepositoryInterface $repository) { + if (intval(getenv('SANDSTORM')) === 1) { + return view('error')->with('message', strval(trans('firefly.sandstorm_not_available'))); + } + + if (auth()->user()->hasRole('demo')) { + Session::flash('info', strval(trans('firefly.cannot_change_demo'))); + + return redirect(route('profile.index')); + } + // old, new1, new2 if (!Hash::check($request->get('current_password'), auth()->user()->password)) { Session::flash('error', strval(trans('firefly.invalid_current_password'))); return redirect(route('profile.change-password')); } - $result = $this->validatePassword($request->get('current_password'), $request->get('new_password')); - if (!($result === true)) { - Session::flash('error', $result); + + try { + $this->validatePassword($request->get('current_password'), $request->get('new_password')); + } catch (ValidationException $e) { + Session::flash('error', $e->getMessage()); return redirect(route('profile.change-password')); } // update the user with the new password. - auth()->user()->password = bcrypt($request->get('new_password')); - auth()->user()->save(); - + $repository->changePassword(auth()->user(), $request->get('new_password')); Session::flash('success', strval(trans('firefly.password_changed'))); - return redirect(route('profile')); + return redirect(route('profile.index')); } /** + * @param UserRepositoryInterface $repository * @param DeleteAccountFormRequest $request * - * @return \Illuminate\Http\RedirectResponse - * @throws \Exception + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function postDeleteAccount(DeleteAccountFormRequest $request) + public function postDeleteAccount(UserRepositoryInterface $repository, DeleteAccountFormRequest $request) { + if (intval(getenv('SANDSTORM')) === 1) { + return view('error')->with('message', strval(trans('firefly.sandstorm_not_available'))); + } + + if (auth()->user()->hasRole('demo')) { + Session::flash('info', strval(trans('firefly.cannot_delete_demo'))); + + return redirect(route('profile.index')); + } + // old, new1, new2 if (!Hash::check($request->get('password'), auth()->user()->password)) { Session::flash('error', strval(trans('firefly.invalid_password'))); return redirect(route('profile.delete-account')); } - - // store some stuff for the future: - $registration = Preferences::get('registration_ip_address')->data; - $confirmation = Preferences::get('confirmation_ip_address')->data; - - // DELETE! - $email = auth()->user()->email; - auth()->user()->delete(); + $user = auth()->user(); + Log::info(sprintf('User #%d has opted to delete their account', auth()->user()->id)); + // make repository delete user: + auth()->logout(); Session::flush(); + $repository->destroy($user); + Session::flash('gaEventCategory', 'user'); Session::flash('gaEventAction', 'delete-account'); - // create a new user with the same email address so re-registration is blocked. - $newUser = User::create( - [ - 'email' => $email, - 'password' => 'deleted', - 'blocked' => 1, - 'blocked_code' => 'deleted', - ] - ); - if (strlen($registration) > 0) { - Preferences::setForUser($newUser, 'registration_ip_address', $registration); - - } - if (strlen($confirmation) > 0) { - Preferences::setForUser($newUser, 'confirmation_ip_address', $confirmation); - } return redirect(route('index')); } /** - * * @param string $old - * @param string $new1 + * @param string $new * - * @return string|bool + * @return bool + * @throws ValidationException */ - protected function validatePassword(string $old, string $new1) + protected function validatePassword(string $old, string $new): bool { - if ($new1 == $old) { - return trans('firefly.should_change'); + if ($new === $old) { + throw new ValidationException(strval(trans('firefly.should_change'))); } return true; diff --git a/app/Http/Controllers/Report/AccountController.php b/app/Http/Controllers/Report/AccountController.php index 582ff0bf83..c36c6c7aea 100644 --- a/app/Http/Controllers/Report/AccountController.php +++ b/app/Http/Controllers/Report/AccountController.php @@ -29,13 +29,13 @@ class AccountController extends Controller { /** + * @param Collection $accounts * @param Carbon $start * @param Carbon $end - * @param Collection $accounts * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @return mixed|string */ - public function accountReport(Carbon $start, Carbon $end, Collection $accounts) + public function general(Collection $accounts, Carbon $start, Carbon $end) { // chart properties for cache: $cache = new CacheProperties; @@ -47,12 +47,13 @@ class AccountController extends Controller return $cache->get(); } - + /** @var AccountTaskerInterface $accountTasker */ $accountTasker = app(AccountTaskerInterface::class); - $accountReport = $accountTasker->getAccountReport($start, $end, $accounts); + $accountReport = $accountTasker->getAccountReport($accounts, $start, $end); $result = view('reports.partials.accounts', compact('accountReport'))->render(); $cache->store($result); + return $result; } } diff --git a/app/Http/Controllers/Report/BalanceController.php b/app/Http/Controllers/Report/BalanceController.php index 74c1d393fb..9dadfa608d 100644 --- a/app/Http/Controllers/Report/BalanceController.php +++ b/app/Http/Controllers/Report/BalanceController.php @@ -30,13 +30,13 @@ class BalanceController extends Controller /** * @param BalanceReportHelperInterface $helper + * @param Collection $accounts * @param Carbon $start * @param Carbon $end - * @param Collection $accounts * - * @return string + * @return mixed|string */ - public function balanceReport(BalanceReportHelperInterface $helper, Carbon $start, Carbon $end, Collection $accounts) + public function general(BalanceReportHelperInterface $helper, Collection $accounts, Carbon $start, Carbon $end) { @@ -50,11 +50,11 @@ class BalanceController extends Controller return $cache->get(); } - $balance = $helper->getBalanceReport($start, $end, $accounts); + $balance = $helper->getBalanceReport($accounts, $start, $end); $result = view('reports.partials.balance', compact('balance'))->render(); $cache->store($result); return $result; } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Report/BudgetController.php b/app/Http/Controllers/Report/BudgetController.php new file mode 100644 index 0000000000..bc4d026631 --- /dev/null +++ b/app/Http/Controllers/Report/BudgetController.php @@ -0,0 +1,123 @@ +addProperty($start); + $cache->addProperty($end); + $cache->addProperty('budget-report'); + $cache->addProperty($accounts->pluck('id')->toArray()); + if ($cache->has()) { + return $cache->get(); + } + + $budgets = $helper->getBudgetReport($start, $end, $accounts); + + $result = view('reports.partials.budgets', compact('budgets'))->render(); + $cache->store($result); + + return $result; + + } + + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return mixed|string + */ + public function period(Collection $accounts, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('budget-period-report'); + $cache->addProperty($accounts->pluck('id')->toArray()); + if ($cache->has()) { + return $cache->get(); + } + + // generate budget report right here. + /** @var BudgetRepositoryInterface $repository */ + $repository = app(BudgetRepositoryInterface::class); + $budgets = $repository->getBudgets(); + $data = $repository->getBudgetPeriodReport($budgets, $accounts, $start, $end); + $data[0] = $repository->getNoBudgetPeriodReport($accounts, $start, $end); // append report data for "no budget" + $report = $this->filterBudgetPeriodReport($data); + $periods = Navigation::listOfPeriods($start, $end); + + $result = view('reports.partials.budget-period', compact('report', 'periods'))->render(); + $cache->store($result); + + return $result; + } + + /** + * Filters empty results from getBudgetPeriodReport + * + * @param array $data + * + * @return array + */ + private function filterBudgetPeriodReport(array $data): array + { + /** + * @var int $budgetId + * @var array $set + */ + foreach ($data as $budgetId => $set) { + $sum = '0'; + foreach ($set['entries'] as $amount) { + $sum = bcadd($amount, $sum); + } + $data[$budgetId]['sum'] = $sum; + if (bccomp('0', $sum) === 0) { + unset($data[$budgetId]); + } + } + + return $data; + } +} diff --git a/app/Http/Controllers/Report/CategoryController.php b/app/Http/Controllers/Report/CategoryController.php index 48ee5d77b7..730deeaece 100644 --- a/app/Http/Controllers/Report/CategoryController.php +++ b/app/Http/Controllers/Report/CategoryController.php @@ -15,10 +15,13 @@ namespace FireflyIII\Http\Controllers\Report; use Carbon\Carbon; -use FireflyIII\Helpers\Report\ReportHelperInterface; use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\Category; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; +use Log; +use Navigation; /** * Class CategoryController @@ -27,16 +30,82 @@ use Illuminate\Support\Collection; */ class CategoryController extends Controller { + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return mixed|string + */ + public function expenses(Collection $accounts, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('category-period-expenses-report'); + $cache->addProperty($accounts->pluck('id')->toArray()); + if ($cache->has()) { + Log::debug('Return report from cache'); + + return $cache->get(); + } + /** @var CategoryRepositoryInterface $repository */ + $repository = app(CategoryRepositoryInterface::class); + $categories = $repository->getCategories(); + $data = $repository->periodExpenses($categories, $accounts, $start, $end); + $data[0] = $repository->periodExpensesNoCategory($accounts, $start, $end); + $report = $this->filterReport($data); + $periods = Navigation::listOfPeriods($start, $end); + $result = view('reports.partials.category-period', compact('report', 'periods'))->render(); + + $cache->store($result); + + return $result; + } /** - * @param ReportHelperInterface $helper - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @param Carbon $start + * @param Carbon $end + * @param Collection $accounts + * + * @return string */ - public function categoryReport(ReportHelperInterface $helper, Carbon $start, Carbon $end, Collection $accounts) + public function income(Collection $accounts, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('category-period-income-report'); + $cache->addProperty($accounts->pluck('id')->toArray()); + if ($cache->has()) { + Log::debug('Return report from cache'); + + return $cache->get(); + } + /** @var CategoryRepositoryInterface $repository */ + $repository = app(CategoryRepositoryInterface::class); + $categories = $repository->getCategories(); + $data = $repository->periodIncome($categories, $accounts, $start, $end); + $data[0] = $repository->periodIncomeNoCategory($accounts, $start, $end); + $report = $this->filterReport($data); + $periods = Navigation::listOfPeriods($start, $end); + $result = view('reports.partials.category-period', compact('report', 'periods'))->render(); + + $cache->store($result); + + return $result; + } + + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return mixed|string + * @internal param ReportHelperInterface $helper + */ + public function operations(Collection $accounts, Carbon $start, Carbon $end) { // chart properties for cache: $cache = new CacheProperties; @@ -48,12 +117,56 @@ class CategoryController extends Controller return $cache->get(); } - $categories = $helper->getCategoryReport($start, $end, $accounts); + /** @var CategoryRepositoryInterface $repository */ + $repository = app(CategoryRepositoryInterface::class); + $categories = $repository->getCategories(); + $report = []; + /** @var Category $category */ + 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]; + } + } - $result = view('reports.partials.categories', compact('categories'))->render(); + // sort the result + // Obtain a list of columns + $sum = []; + foreach ($report as $categoryId => $row) { + $sum[$categoryId] = floatval($row['spent']); + } + + array_multisort($sum, SORT_ASC, $report); + + $result = view('reports.partials.categories', compact('report'))->render(); $cache->store($result); return $result; } -} \ No newline at end of file + /** + * Filters empty results from category period report + * + * @param array $data + * + * @return array + */ + private function filterReport(array $data): array + { + foreach ($data as $categoryId => $set) { + $sum = '0'; + foreach ($set['entries'] as $amount) { + $sum = bcadd($amount, $sum); + } + $data[$categoryId]['sum'] = $sum; + if (bccomp('0', $sum) === 0) { + unset($data[$categoryId]); + } + } + + + return $data; + } + + +} diff --git a/app/Http/Controllers/Report/InOutController.php b/app/Http/Controllers/Report/InOutController.php deleted file mode 100644 index abc23c55fb..0000000000 --- a/app/Http/Controllers/Report/InOutController.php +++ /dev/null @@ -1,66 +0,0 @@ -addProperty($start); - $cache->addProperty($end); - $cache->addProperty('in-out-report'); - $cache->addProperty($accounts->pluck('id')->toArray()); - if ($cache->has()) { - return Response::json($cache->get()); - } - - $incomes = $helper->getIncomeReport($start, $end, $accounts); - $expenses = $helper->getExpenseReport($start, $end, $accounts); - - $result = [ - 'income' => view('reports.partials.income', compact('incomes'))->render(), - 'expenses' => view('reports.partials.expenses', compact('expenses'))->render(), - 'incomes_expenses' => view('reports.partials.income-vs-expenses', compact('expenses', 'incomes'))->render(), - ]; - $cache->store($result); - - return Response::json($result); - - } - -} \ No newline at end of file diff --git a/app/Http/Controllers/Report/OperationsController.php b/app/Http/Controllers/Report/OperationsController.php new file mode 100644 index 0000000000..f1abe0b96e --- /dev/null +++ b/app/Http/Controllers/Report/OperationsController.php @@ -0,0 +1,253 @@ +addProperty($start); + $cache->addProperty($end); + $cache->addProperty('expense-report'); + $cache->addProperty($accounts->pluck('id')->toArray()); + if ($cache->has()) { + return $cache->get(); + } + $entries = $this->getExpenseReport($start, $end, $accounts); + $type = 'expense-entry'; + $result = view('reports.partials.income-expenses', compact('entries', 'type'))->render(); + $cache->store($result); + + return $result; + + } + + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function income(Collection $accounts, Carbon $start, Carbon $end) + { + // chart properties for cache: + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('income-report'); + $cache->addProperty($accounts->pluck('id')->toArray()); + if ($cache->has()) { + return $cache->get(); + } + $entries = $this->getIncomeReport($start, $end, $accounts); + $type = 'income-entry'; + $result = view('reports.partials.income-expenses', compact('entries', 'type'))->render(); + + $cache->store($result); + + return $result; + + } + + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return mixed|string + */ + public function operations(Collection $accounts, Carbon $start, Carbon $end) + { + // chart properties for cache: + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('inc-exp-report'); + $cache->addProperty($accounts->pluck('id')->toArray()); + if ($cache->has()) { + return $cache->get(); + } + + $incomes = $this->getIncomeReport($start, $end, $accounts); + $expenses = $this->getExpenseReport($start, $end, $accounts); + $incomeSum = array_sum( + array_map( + function ($item) { + return $item['sum']; + }, $incomes + ) + ); + + $expensesSum = array_sum( + array_map( + function ($item) { + return $item['sum']; + }, $expenses + ) + ); + + $result = view('reports.partials.operations', compact('incomeSum', 'expensesSum'))->render(); + $cache->store($result); + + return $result; + + } + + /** + * @param Carbon $start + * @param Carbon $end + * @param Collection $accounts + * + * @return array + */ + private function getExpenseReport(Carbon $start, Carbon $end, Collection $accounts): array + { + // get all expenses for the given accounts in the given period! + // also transfers! + // get all transactions: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end); + $collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) + ->withOpposingAccount() + ->enableInternalFilter(); + $transactions = $collector->getJournals(); + $transactions = $transactions->filter( + function (Transaction $transaction) { + // return negative amounts only. + if (bccomp($transaction->transaction_amount, '0') === -1) { + return $transaction; + } + + return false; + } + ); + $expenses = $this->groupByOpposing($transactions); + + // sort the result + // Obtain a list of columns + $sum = []; + foreach ($expenses as $accountId => $row) { + $sum[$accountId] = floatval($row['sum']); + } + + array_multisort($sum, SORT_ASC, $expenses); + + return $expenses; + } + + /** + * @param Carbon $start + * @param Carbon $end + * @param Collection $accounts + * + * @return array + */ + private function getIncomeReport(Carbon $start, Carbon $end, Collection $accounts): array + { + // get all expenses for the given accounts in the given period! + // also transfers! + // get all transactions: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end); + $collector->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) + ->withOpposingAccount() + ->enableInternalFilter(); + $transactions = $collector->getJournals(); + $transactions = $transactions->filter( + function (Transaction $transaction) { + // return positive amounts only. + if (bccomp($transaction->transaction_amount, '0') === 1) { + return $transaction; + } + + return false; + } + ); + $income = $this->groupByOpposing($transactions); + + // sort the result + // Obtain a list of columns + $sum = []; + foreach ($income as $accountId => $row) { + $sum[$accountId] = floatval($row['sum']); + } + + array_multisort($sum, SORT_DESC, $income); + + return $income; + } + + /** + * @param Collection $transactions + * + * @return array + */ + private function groupByOpposing(Collection $transactions): array + { + $expenses = []; + // join the result together: + foreach ($transactions as $transaction) { + $opposingId = $transaction->opposing_account_id; + $name = $transaction->opposing_account_name; + if (!isset($expenses[$opposingId])) { + $expenses[$opposingId] = [ + 'id' => $opposingId, + 'name' => $name, + 'sum' => '0', + 'average' => '0', + 'count' => 0, + ]; + } + $expenses[$opposingId]['sum'] = bcadd($expenses[$opposingId]['sum'], $transaction->transaction_amount); + $expenses[$opposingId]['count']++; + } + // do averages: + foreach ($expenses as $key => $entry) { + if ($expenses[$key]['count'] > 1) { + $expenses[$key]['average'] = bcdiv($expenses[$key]['sum'], strval($expenses[$key]['count'])); + } + } + + + return $expenses; + } + +} diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 3bdeaf7f44..5c32e086f9 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -15,19 +15,20 @@ namespace FireflyIII\Http\Controllers; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Helpers\Report\BudgetReportHelperInterface; +use FireflyIII\Generator\Report\ReportGeneratorFactory; use FireflyIII\Helpers\Report\ReportHelperInterface; -use FireflyIII\Models\Account; +use FireflyIII\Http\Requests\ReportFormRequest; use FireflyIII\Models\AccountType; -use FireflyIII\Models\Transaction; +use FireflyIII\Models\Tag; use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; +use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use Illuminate\Http\RedirectResponse; use Illuminate\Support\Collection; use Preferences; +use Response; use Session; -use Steam; use View; /** @@ -37,8 +38,6 @@ use View; */ class ReportController extends Controller { - /** @var BudgetReportHelperInterface */ - protected $budgetHelper; /** @var ReportHelperInterface */ protected $helper; @@ -49,8 +48,196 @@ class ReportController extends Controller { parent::__construct(); - View::share('title', trans('firefly.reports')); - View::share('mainTitleIcon', 'fa-line-chart'); + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.reports')); + View::share('mainTitleIcon', 'fa-line-chart'); + View::share('subTitleIcon', 'fa-calendar'); + + $this->helper = app(ReportHelperInterface::class); + + return $next($request); + } + ); + + } + + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function auditReport(Collection $accounts, Carbon $start, Carbon $end) + { + if ($end < $start) { + return view('error')->with('message', trans('firefly.end_after_start_date')); + } + if ($start < session('first')) { + $start = session('first'); + } + + View::share( + 'subTitle', trans( + 'firefly.report_audit', + [ + 'start' => $start->formatLocalized($this->monthFormat), + 'end' => $end->formatLocalized($this->monthFormat), + ] + ) + ); + + + $generator = ReportGeneratorFactory::reportGenerator('Audit', $start, $end); + $generator->setAccounts($accounts); + $result = $generator->generate(); + + return $result; + + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function tagReport(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + if ($end < $start) { + return view('error')->with('message', trans('firefly.end_after_start_date')); + } + if ($start < session('first')) { + $start = session('first'); + } + + View::share( + 'subTitle', trans( + 'firefly.report_tag', + [ + 'start' => $start->formatLocalized($this->monthFormat), + 'end' => $end->formatLocalized($this->monthFormat), + ] + ) + ); + + $generator = ReportGeneratorFactory::reportGenerator('Tag', $start, $end); + $generator->setAccounts($accounts); + $generator->setTags($tags); + $result = $generator->generate(); + + return $result; + + } + + /** + * @param Collection $accounts + * @param Collection $budgets + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function budgetReport(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end) + { + if ($end < $start) { + return view('error')->with('message', trans('firefly.end_after_start_date')); + } + if ($start < session('first')) { + $start = session('first'); + } + + View::share( + 'subTitle', trans( + 'firefly.report_budget', + [ + 'start' => $start->formatLocalized($this->monthFormat), + 'end' => $end->formatLocalized($this->monthFormat), + ] + ) + ); + + $generator = ReportGeneratorFactory::reportGenerator('Budget', $start, $end); + $generator->setAccounts($accounts); + $generator->setBudgets($budgets); + $result = $generator->generate(); + + return $result; + + } + + /** + * @param Collection $accounts + * @param Collection $categories + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function categoryReport(Collection $accounts, Collection $categories, Carbon $start, Carbon $end) + { + if ($end < $start) { + return view('error')->with('message', trans('firefly.end_after_start_date')); + } + if ($start < session('first')) { + $start = session('first'); + } + + View::share( + 'subTitle', trans( + 'firefly.report_category', + [ + 'start' => $start->formatLocalized($this->monthFormat), + 'end' => $end->formatLocalized($this->monthFormat), + ] + ) + ); + + $generator = ReportGeneratorFactory::reportGenerator('Category', $start, $end); + $generator->setAccounts($accounts); + $generator->setCategories($categories); + $result = $generator->generate(); + + return $result; + + } + + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function defaultReport(Collection $accounts, Carbon $start, Carbon $end) + { + if ($end < $start) { + return view('error')->with('message', trans('firefly.end_after_start_date')); + } + + if ($start < session('first')) { + $start = session('first'); + } + + View::share( + 'subTitle', trans( + 'firefly.report_default', + [ + 'start' => $start->formatLocalized($this->monthFormat), + 'end' => $end->formatLocalized($this->monthFormat), + ] + ) + ); + + $generator = ReportGeneratorFactory::reportGenerator('Standard', $start, $end); + $generator->setAccounts($accounts); + $result = $generator->generate(); + + return $result; } @@ -61,41 +248,85 @@ class ReportController extends Controller */ public function index(AccountRepositoryInterface $repository) { - $this->createRepositories(); /** @var Carbon $start */ $start = clone session('first'); $months = $this->helper->listOfMonths($start); $customFiscalYear = Preferences::get('customFiscalYear', 0)->data; - - // does the user have shared accounts? - $accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); - // get id's for quick links: - $accountIds = []; - /** @var Account $account */ - foreach ($accounts as $account) { - $accountIds [] = $account->id; - } - $accountList = join(',', $accountIds); + $accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + $accountList = join(',', $accounts->pluck('id')->toArray()); return view('reports.index', compact('months', 'accounts', 'start', 'accountList', 'customFiscalYear')); } /** - * @param string $reportType - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts + * @param string $reportType * - * @return View + * @return mixed + */ + public function options(string $reportType) + { + switch ($reportType) { + default: + $result = $this->noReportOptions(); + break; + case 'category': + $result = $this->categoryReportOptions(); + break; + case 'budget': + $result = $this->budgetReportOptions(); + break; + case 'tag': + $result = $this->tagReportOptions(); + break; + } + + return Response::json(['html' => $result]); + } + + /** + * @param ReportFormRequest $request + * + * @return RedirectResponse * @throws FireflyException */ - public function report(string $reportType, Carbon $start, Carbon $end, Collection $accounts) + public function postIndex(ReportFormRequest $request): RedirectResponse { - $this->createRepositories(); - // throw an error if necessary. + // report type: + $reportType = $request->get('report_type'); + $start = $request->getStartDate()->format('Ymd'); + $end = $request->getEndDate()->format('Ymd'); + $accounts = join(',', $request->getAccountList()->pluck('id')->toArray()); + $categories = join(',', $request->getCategoryList()->pluck('id')->toArray()); + $budgets = join(',', $request->getBudgetList()->pluck('id')->toArray()); + $tags = join(',', $request->getTagList()->pluck('tag')->toArray()); + + if ($request->getAccountList()->count() === 0) { + Session::flash('error', trans('firefly.select_more_than_one_account')); + + return redirect(route('reports.index')); + } + + if ($request->getCategoryList()->count() === 0 && $reportType === 'category') { + Session::flash('error', trans('firefly.select_more_than_one_category')); + + return redirect(route('reports.index')); + } + + if ($request->getBudgetList()->count() === 0 && $reportType === 'budget') { + Session::flash('error', trans('firefly.select_more_than_one_budget')); + + return redirect(route('reports.index')); + } + + if ($request->getTagList()->count() === 0 && $reportType === 'tag') { + Session::flash('error', trans('firefly.select_more_than_one_tag')); + + return redirect(route('reports.index')); + } + if ($end < $start) { - throw new FireflyException('End date cannot be before start date, silly!'); + return view('error')->with('message', trans('firefly.end_after_start_date')); } // lower threshold @@ -103,204 +334,80 @@ class ReportController extends Controller $start = session('first'); } - View::share( - 'subTitle', trans( - 'firefly.report_' . $reportType, - [ - 'start' => $start->formatLocalized($this->monthFormat), - 'end' => $end->formatLocalized($this->monthFormat), - ] - ) - ); - View::share('subTitleIcon', 'fa-calendar'); - switch ($reportType) { default: - throw new FireflyException('Unfortunately, reports of the type "' . e($reportType) . '" are not available at this time.'); + throw new FireflyException(sprintf('Firefly does not support the "%s"-report yet.', $reportType)); + case 'category': + $uri = route('reports.report.category', [$accounts, $categories, $start, $end]); + break; case 'default': - - // more than one year date difference means year report. - if ($start->diffInMonths($end) > 12) { - return $this->defaultMultiYear($reportType, $start, $end, $accounts); - } - // more than two months date difference means year report. - if ($start->diffInMonths($end) > 1) { - return $this->defaultYear($reportType, $start, $end, $accounts); - } - - // otherwise default - return $this->defaultMonth($reportType, $start, $end, $accounts); + $uri = route('reports.report.default', [$accounts, $start, $end]); + break; case 'audit': - // always default - return $this->auditReport($start, $end, $accounts); + $uri = route('reports.report.audit', [$accounts, $start, $end]); + break; + case 'budget': + $uri = route('reports.report.budget', [$accounts, $budgets, $start, $end]); + break; + case 'tag': + $uri = route('reports.report.tag', [$accounts, $tags, $start, $end]); + break; } + return redirect($uri); + } + + /** + * @return string + */ + private function budgetReportOptions(): string + { + /** @var BudgetRepositoryInterface $repository */ + $repository = app(BudgetRepositoryInterface::class); + $budgets = $repository->getBudgets(); + $result = view('reports.options.budget', compact('budgets'))->render(); + + return $result; } /** - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return View + * @return string */ - private function auditReport(Carbon $start, Carbon $end, Collection $accounts) + private function categoryReportOptions(): string { - /** @var AccountTaskerInterface $tasker */ - $tasker = app(AccountTaskerInterface::class); - $auditData = []; - $dayBefore = clone $start; - $dayBefore->subDay(); - /** @var Account $account */ - foreach ($accounts as $account) { - // balance the day before: - $id = $account->id; - $dayBeforeBalance = Steam::balance($account, $dayBefore); - $journals = $tasker->getJournalsInPeriod(new Collection([$account]), [], $start, $end); - $journals = $journals->reverse(); - $startBalance = $dayBeforeBalance; + /** @var CategoryRepositoryInterface $repository */ + $repository = app(CategoryRepositoryInterface::class); + $categories = $repository->getCategories(); + $result = view('reports.options.category', compact('categories'))->render(); + return $result; - /** @var Transaction $journal */ - foreach ($journals as $transaction) { - $transaction->before = $startBalance; - $transactionAmount = $transaction->transaction_amount; - $newBalance = bcadd($startBalance, $transactionAmount); - $transaction->after = $newBalance; - $startBalance = $newBalance; + } + + /** + * @return string + */ + private function noReportOptions(): string + { + return view('reports.options.no-options')->render(); + } + + /** + * @return string + */ + private function tagReportOptions(): string + { + /** @var TagRepositoryInterface $repository */ + $repository = app(TagRepositoryInterface::class); + $tags = $repository->get()->sortBy( + function (Tag $tag) { + return $tag->tag; } - - /* - * Reverse set again. - */ - $auditData[$id]['journals'] = $journals->reverse(); - $auditData[$id]['exists'] = $journals->count() > 0; - $auditData[$id]['end'] = $end->formatLocalized(strval(trans('config.month_and_day'))); - $auditData[$id]['endBalance'] = Steam::balance($account, $end); - $auditData[$id]['dayBefore'] = $dayBefore->formatLocalized(strval(trans('config.month_and_day'))); - $auditData[$id]['dayBeforeBalance'] = $dayBeforeBalance; - } - - $reportType = 'audit'; - $accountIds = join(',', $accounts->pluck('id')->toArray()); - - $hideable = ['buttons', 'icon', 'description', 'balance_before', 'amount', 'balance_after', 'date', - 'interest_date', 'book_date', 'process_date', - // three new optional fields. - 'due_date', 'payment_date', 'invoice_date', - 'from', 'to', 'budget', 'category', 'bill', - // more new optional fields - 'internal_reference', 'notes', - - 'create_date', 'update_date', - ]; - $defaultShow = ['icon', 'description', 'balance_before', 'amount', 'balance_after', 'date', 'to']; - - return view('reports.audit.report', compact('start', 'end', 'reportType', 'accountIds', 'accounts', 'auditData', 'hideable', 'defaultShow')); - } - - /** - * - */ - private function createRepositories() - { - $this->helper = app(ReportHelperInterface::class); - $this->budgetHelper = app(BudgetReportHelperInterface::class); - } - - /** - * @param $reportType - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return View - */ - private function defaultMonth(string $reportType, Carbon $start, Carbon $end, Collection $accounts) - { - // get report stuff! - $budgets = $this->budgetHelper->getBudgetReport($start, $end, $accounts); - $bills = $this->helper->getBillReport($start, $end, $accounts); - $tags = $this->helper->tagReport($start, $end, $accounts); - - // and some id's, joined: - $accountIds = join(',', $accounts->pluck('id')->toArray()); - - // continue! - return view( - 'reports.default.month', - compact( - 'start', 'end', - 'tags', - 'budgets', - 'bills', - 'accountIds', 'reportType' - ) ); - } + $result = view('reports.options.tag', compact('tags'))->render(); - /** - * @param $reportType - * @param $start - * @param $end - * @param $accounts - * - * @return View - */ - private function defaultMultiYear(string $reportType, Carbon $start, Carbon $end, Collection $accounts) - { + return $result; - $budgets = app(BudgetRepositoryInterface::class)->getActiveBudgets(); - $categories = app(CategoryRepositoryInterface::class)->getCategories(); - $tags = $this->helper->tagReport($start, $end, $accounts); - - // and some id's, joined: - $accountIds = []; - /** @var Account $account */ - foreach ($accounts as $account) { - $accountIds[] = $account->id; - } - $accountIds = join(',', $accountIds); - - return view( - 'reports.default.multi-year', - compact( - 'budgets', 'accounts', 'categories', 'start', 'end', 'accountIds', 'reportType', 'tags' - ) - ); - } - - /** - * @param $reportType - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return View - */ - private function defaultYear(string $reportType, Carbon $start, Carbon $end, Collection $accounts) - { - $tags = $this->helper->tagReport($start, $end, $accounts); - $budgets = $this->budgetHelper->budgetYearOverview($start, $end, $accounts); - - Session::flash('gaEventCategory', 'report'); - Session::flash('gaEventAction', 'year'); - Session::flash('gaEventLabel', $start->format('Y')); - - // and some id's, joined: - $accountIds = []; - /** @var Account $account */ - foreach ($accounts as $account) { - $accountIds[] = $account->id; - } - $accountIds = join(',', $accountIds); - - return view( - 'reports.default.year', - compact( - 'start', 'reportType', 'accountIds', 'end', 'tags', 'budgets' - ) - ); } } diff --git a/app/Http/Controllers/RuleController.php b/app/Http/Controllers/RuleController.php index 4b0687a559..e20830a819 100644 --- a/app/Http/Controllers/RuleController.php +++ b/app/Http/Controllers/RuleController.php @@ -22,11 +22,10 @@ use FireflyIII\Models\RuleTrigger; use FireflyIII\Repositories\Rule\RuleRepositoryInterface; use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; use FireflyIII\Rules\TransactionMatcher; -use Input; +use Illuminate\Http\Request; use Preferences; use Response; use Session; -use URL; use View; /** @@ -42,18 +41,27 @@ class RuleController extends Controller public function __construct() { parent::__construct(); - View::share('title', trans('firefly.rules')); - View::share('mainTitleIcon', 'fa-random'); + + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.rules')); + View::share('mainTitleIcon', 'fa-random'); + + return $next($request); + } + ); } /** * Create a new rule. It will be stored under the given $ruleGroup. * + * @param Request $request * @param RuleGroup $ruleGroup * * @return View */ - public function create(RuleGroup $ruleGroup) + public function create(Request $request, RuleGroup $ruleGroup) { // count for possible present previous entered triggers/actions. $triggerCount = 0; @@ -64,13 +72,13 @@ class RuleController extends Controller $oldActions = []; // has old input? - if (Input::old()) { + if ($request->old()) { // process old triggers. - $oldTriggers = $this->getPreviousTriggers(); + $oldTriggers = $this->getPreviousTriggers($request); $triggerCount = count($oldTriggers); // process old actions - $oldActions = $this->getPreviousActions(); + $oldActions = $this->getPreviousActions($request); $actionCount = count($oldActions); } @@ -78,10 +86,10 @@ class RuleController extends Controller $subTitle = trans('firefly.make_new_rule', ['title' => $ruleGroup->title]); // put previous url in session if not redirect from store (not "create another"). - if (session('rules.rule.create.fromStore') !== true) { - Session::put('rules.rule.create.url', URL::previous()); + if (session('rules.create.fromStore') !== true) { + $this->rememberPreviousUri('rules.create.uri'); } - Session::forget('rules.rule.create.fromStore'); + Session::forget('rules.create.fromStore'); Session::flash('gaEventCategory', 'rules'); Session::flash('gaEventAction', 'create-rule'); @@ -96,14 +104,13 @@ class RuleController extends Controller * @param Rule $rule * * @return View - * @internal param RuleRepositoryInterface $repository */ public function delete(Rule $rule) { $subTitle = trans('firefly.delete_rule', ['title' => $rule->title]); // put previous url in session - Session::put('rules.rule.delete.url', URL::previous()); + $this->rememberPreviousUri('rules.delete.uri'); Session::flash('gaEventCategory', 'rules'); Session::flash('gaEventAction', 'delete-rule'); @@ -127,8 +134,7 @@ class RuleController extends Controller Session::flash('success', trans('firefly.deleted_rule', ['title' => $title])); Preferences::mark(); - - return redirect(session('rules.rule.delete.url')); + return redirect($this->getPreviousUri('rules.delete.uri')); } /** @@ -146,12 +152,13 @@ class RuleController extends Controller } /** + * @param Request $request * @param RuleRepositoryInterface $repository * @param Rule $rule * * @return View */ - public function edit(RuleRepositoryInterface $repository, Rule $rule) + public function edit(Request $request, RuleRepositoryInterface $repository, Rule $rule) { $oldTriggers = $this->getCurrentTriggers($rule); $triggerCount = count($oldTriggers); @@ -159,10 +166,10 @@ class RuleController extends Controller $actionCount = count($oldActions); // has old input? - if (Input::old()) { - $oldTriggers = $this->getPreviousTriggers(); + if ($request->old()) { + $oldTriggers = $this->getPreviousTriggers($request); $triggerCount = count($oldTriggers); - $oldActions = $this->getPreviousActions(); + $oldActions = $this->getPreviousActions($request); $actionCount = count($oldActions); } @@ -171,10 +178,10 @@ class RuleController extends Controller $subTitle = trans('firefly.edit_rule', ['title' => $rule->title]); // put previous url in session if not redirect from store (not "return_to_edit"). - if (session('rules.rule.edit.fromUpdate') !== true) { - Session::put('rules.rule.edit.url', URL::previous()); + if (session('rules.edit.fromUpdate') !== true) { + $this->rememberPreviousUri('rules.edit.uri'); } - Session::forget('rules.rule.edit.fromUpdate'); + Session::forget('rules.edit.fromUpdate'); Session::flash('gaEventCategory', 'rules'); Session::flash('gaEventAction', 'edit-rule'); @@ -196,14 +203,15 @@ class RuleController extends Controller } /** + * @param Request $request * @param RuleRepositoryInterface $repository * @param Rule $rule * * @return \Illuminate\Http\JsonResponse */ - public function reorderRuleActions(RuleRepositoryInterface $repository, Rule $rule) + public function reorderRuleActions(Request $request, RuleRepositoryInterface $repository, Rule $rule) { - $ids = Input::get('actions'); + $ids = $request->get('actions'); if (is_array($ids)) { $repository->reorderRuleActions($rule, $ids); } @@ -213,14 +221,15 @@ class RuleController extends Controller } /** + * @param Request $request * @param RuleRepositoryInterface $repository * @param Rule $rule * * @return \Illuminate\Http\JsonResponse */ - public function reorderRuleTriggers(RuleRepositoryInterface $repository, Rule $rule) + public function reorderRuleTriggers(Request $request, RuleRepositoryInterface $repository, Rule $rule) { - $ids = Input::get('triggers'); + $ids = $request->get('triggers'); if (is_array($ids)) { $repository->reorderRuleTriggers($rule, $ids); } @@ -245,16 +254,14 @@ class RuleController extends Controller Session::flash('success', trans('firefly.stored_new_rule', ['title' => $rule->title])); Preferences::mark(); - if (intval(Input::get('create_another')) === 1) { + if (intval($request->get('create_another')) === 1) { // set value so create routine will not overwrite URL: - Session::put('rules.rule.create.fromStore', true); + Session::put('rules.create.fromStore', true); - return redirect(route('rules.rule.create', [$ruleGroup]))->withInput(); + return redirect(route('rules.create', [$ruleGroup]))->withInput(); } - // redirect to previous URL. - return redirect(session('rules.rule.create.url')); - + return redirect($this->getPreviousUri('rules.create.uri')); } /** @@ -282,7 +289,7 @@ class RuleController extends Controller $range = config('firefly.test-triggers.range'); /** @var TransactionMatcher $matcher */ - $matcher = app('FireflyIII\Rules\TransactionMatcher'); + $matcher = app(TransactionMatcher::class); $matcher->setLimit($limit); $matcher->setRange($range); $matcher->setTriggers($triggers); @@ -298,7 +305,7 @@ class RuleController extends Controller } // Return json response - $view = view('list.journals-tiny', ['transactions' => $matchingTransactions])->render(); + $view = view('list.journals-tiny-tasker', ['transactions' => $matchingTransactions])->render(); return Response::json(['html' => $view, 'warning' => $warning]); } @@ -332,21 +339,20 @@ class RuleController extends Controller Session::flash('success', trans('firefly.updated_rule', ['title' => $rule->title])); Preferences::mark(); - if (intval(Input::get('return_to_edit')) === 1) { + if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: - Session::put('rules.rule.edit.fromUpdate', true); + Session::put('rules.edit.fromUpdate', true); - return redirect(route('rules.rule.edit', [$rule->id]))->withInput(['return_to_edit' => 1]); + return redirect(route('rules.edit', [$rule->id]))->withInput(['return_to_edit' => 1]); } - // redirect to previous URL. - return redirect(session('rules.rule.edit.url')); + return redirect($this->getPreviousUri('rules.edit.uri')); } private function createDefaultRule() { /** @var RuleRepositoryInterface $repository */ - $repository = app('FireflyIII\Repositories\Rule\RuleRepositoryInterface'); + $repository = app(RuleRepositoryInterface::class); if ($repository->count() === 0) { $data = [ @@ -451,22 +457,24 @@ class RuleController extends Controller } /** + * @param Request $request + * * @return array */ - private function getPreviousActions() + private function getPreviousActions(Request $request) { $newIndex = 0; $actions = []; /** @var array $oldActions */ - $oldActions = is_array(Input::old('rule-action')) ? Input::old('rule-action') : []; + $oldActions = is_array($request->old('rule-action')) ? $request->old('rule-action') : []; foreach ($oldActions as $index => $entry) { $count = ($newIndex + 1); - $checked = isset(Input::old('rule-action-stop')[$index]) ? true : false; + $checked = isset($request->old('rule-action-stop')[$index]) ? true : false; $actions[] = view( 'rules.partials.action', [ 'oldTrigger' => $entry, - 'oldValue' => Input::old('rule-action-value')[$index], + 'oldValue' => $request->old('rule-action-value')[$index], 'oldChecked' => $checked, 'count' => $count, ] @@ -478,22 +486,24 @@ class RuleController extends Controller } /** + * @param Request $request + * * @return array */ - private function getPreviousTriggers() + private function getPreviousTriggers(Request $request) { $newIndex = 0; $triggers = []; /** @var array $oldTriggers */ - $oldTriggers = is_array(Input::old('rule-trigger')) ? Input::old('rule-trigger') : []; + $oldTriggers = is_array($request->old('rule-trigger')) ? $request->old('rule-trigger') : []; foreach ($oldTriggers as $index => $entry) { $count = ($newIndex + 1); - $oldChecked = isset(Input::old('rule-trigger-stop')[$index]) ? true : false; + $oldChecked = isset($request->old('rule-trigger-stop')[$index]) ? true : false; $triggers[] = view( 'rules.partials.trigger', [ 'oldTrigger' => $entry, - 'oldValue' => Input::old('rule-trigger-value')[$index], + 'oldValue' => $request->old('rule-trigger-value')[$index], 'oldChecked' => $oldChecked, 'count' => $count, ] diff --git a/app/Http/Controllers/RuleGroupController.php b/app/Http/Controllers/RuleGroupController.php index af62ca2085..e0bc8b7476 100644 --- a/app/Http/Controllers/RuleGroupController.php +++ b/app/Http/Controllers/RuleGroupController.php @@ -22,10 +22,9 @@ use FireflyIII\Models\AccountType; use FireflyIII\Models\RuleGroup; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; -use Input; +use Illuminate\Http\Request; use Preferences; use Session; -use URL; use View; /** @@ -41,8 +40,16 @@ class RuleGroupController extends Controller public function __construct() { parent::__construct(); - View::share('title', trans('firefly.rules')); - View::share('mainTitleIcon', 'fa-random'); + + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.rules')); + View::share('mainTitleIcon', 'fa-random'); + + return $next($request); + } + ); } /** @@ -54,10 +61,10 @@ class RuleGroupController extends Controller $subTitle = trans('firefly.make_new_rule_group'); // put previous url in session if not redirect from store (not "create another"). - if (session('rules.rule-group.create.fromStore') !== true) { - Session::put('rules.rule-group.create.url', URL::previous()); + if (session('rule-groups.create.fromStore') !== true) { + $this->rememberPreviousUri('rule-groups.create.uri'); } - Session::forget('rules.rule-group.create.fromStore'); + Session::forget('rule-groups.create.fromStore'); Session::flash('gaEventCategory', 'rules'); Session::flash('gaEventAction', 'create-rule-group'); @@ -78,7 +85,7 @@ class RuleGroupController extends Controller unset($ruleGroupList[$ruleGroup->id]); // put previous url in session - Session::put('rules.rule-group.delete.url', URL::previous()); + $this->rememberPreviousUri('rule-groups.delete.uri'); Session::flash('gaEventCategory', 'rules'); Session::flash('gaEventAction', 'delete-rule-group'); @@ -86,17 +93,17 @@ class RuleGroupController extends Controller } /** + * @param Request $request * @param RuleGroupRepositoryInterface $repository - * * @param RuleGroup $ruleGroup * - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function destroy(RuleGroupRepositoryInterface $repository, RuleGroup $ruleGroup) + public function destroy(Request $request, RuleGroupRepositoryInterface $repository, RuleGroup $ruleGroup) { $title = $ruleGroup->title; - $moveTo = auth()->user()->ruleGroups()->find(intval(Input::get('move_rules_before_delete'))); + $moveTo = auth()->user()->ruleGroups()->find(intval($request->get('move_rules_before_delete'))); $repository->destroy($ruleGroup, $moveTo); @@ -104,8 +111,7 @@ class RuleGroupController extends Controller Session::flash('success', strval(trans('firefly.deleted_rule_group', ['title' => $title]))); Preferences::mark(); - - return redirect(session('rules.rule-group.delete.url')); + return redirect($this->getPreviousUri('rule-groups.delete.uri')); } /** @@ -132,10 +138,10 @@ class RuleGroupController extends Controller $subTitle = trans('firefly.edit_rule_group', ['title' => $ruleGroup->title]); // put previous url in session if not redirect from store (not "return_to_edit"). - if (session('rules.rule-group.edit.fromUpdate') !== true) { - Session::put('rules.rule-group.edit.url', URL::previous()); + if (session('rule-groups.edit.fromUpdate') !== true) { + $this->rememberPreviousUri('rule-groups.edit.uri'); } - Session::forget('rules.rule-group.edit.fromUpdate'); + Session::forget('rule-groups.edit.fromUpdate'); Session::flash('gaEventCategory', 'rules'); Session::flash('gaEventAction', 'edit-rule-group'); @@ -204,21 +210,20 @@ class RuleGroupController extends Controller */ public function store(RuleGroupFormRequest $request, RuleGroupRepositoryInterface $repository) { - $data = $request->getRuleGroupData(); - $ruleGroup = $repository->store($data); + $data = $request->getRuleGroupData(); + $ruleGroup = $repository->store($data); Session::flash('success', strval(trans('firefly.created_new_rule_group', ['title' => $ruleGroup->title]))); Preferences::mark(); - if (intval(Input::get('create_another')) === 1) { + if (intval($request->get('create_another')) === 1) { // set value so create routine will not overwrite URL: - Session::put('rules.rule-group.create.fromStore', true); + Session::put('rule-groups.create.fromStore', true); - return redirect(route('rules.rule-group.create'))->withInput(); + return redirect(route('rule-groups.create'))->withInput(); } - // redirect to previous URL. - return redirect(session('rules.rule-group.create.url')); + return redirect($this->getPreviousUri('rule-groups.create.uri')); } /** @@ -255,15 +260,15 @@ class RuleGroupController extends Controller Session::flash('success', strval(trans('firefly.updated_rule_group', ['title' => $ruleGroup->title]))); Preferences::mark(); - if (intval(Input::get('return_to_edit')) === 1) { + if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: - Session::put('rules.rule-group.edit.fromUpdate', true); + Session::put('rule-groups.edit.fromUpdate', true); - return redirect(route('rules.rule-group.edit', [$ruleGroup->id]))->withInput(['return_to_edit' => 1]); + return redirect(route('rule-groups.edit', [$ruleGroup->id]))->withInput(['return_to_edit' => 1]); } // redirect to previous URL. - return redirect(session('rules.rule-group.edit.url')); + return redirect($this->getPreviousUri('rule-groups.edit.uri')); } } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 2066537371..4b47777c62 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -14,7 +14,7 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers; use FireflyIII\Support\Search\SearchInterface; -use Input; +use Illuminate\Http\Request; /** * Class SearchController @@ -29,25 +29,32 @@ class SearchController extends Controller public function __construct() { parent::__construct(); + } /** * Results always come in the form of an array [results, count, fullCount] * + * @param Request $request * @param SearchInterface $searcher * * @return $this */ - public function index(SearchInterface $searcher) + public function index(Request $request, SearchInterface $searcher) { - + $minSearchLen = 1; $subTitle = null; $query = null; $result = []; $title = trans('firefly.search'); + $limit = 20; $mainTitleIcon = 'fa-search'; - if (!is_null(Input::get('q')) && strlen(Input::get('q')) > 0) { - $query = trim(Input::get('q')); + + // set limit for search: + $searcher->setLimit($limit); + + if (!is_null($request->get('q')) && strlen($request->get('q')) >= $minSearchLen) { + $query = trim(strtolower($request->get('q'))); $words = explode(' ', $query); $subTitle = trans('firefly.search_results_for', ['query' => $query]); @@ -60,7 +67,7 @@ class SearchController extends Controller } - return view('search.index', compact('title', 'subTitle', 'mainTitleIcon', 'query', 'result')); + return view('search.index', compact('title', 'subTitle', 'limit', 'mainTitleIcon', 'query', 'result')); } } diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index 8459a12af8..bdb3160ea7 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -13,17 +13,19 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers; +use Carbon\Carbon; +use Exception; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Requests\TagFormRequest; -use FireflyIII\Models\Preference; use FireflyIII\Models\Tag; -use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\Transaction; use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use FireflyIII\Support\CacheProperties; +use Illuminate\Http\Request; use Illuminate\Support\Collection; -use Input; +use Navigation; use Preferences; -use Response; use Session; -use URL; use View; /** @@ -42,48 +44,66 @@ use View; class TagController extends Controller { + /** @var array */ public $tagOptions = []; + /** @var TagRepositoryInterface */ + protected $repository; + /** * */ public function __construct() { parent::__construct(); - View::share('title', 'Tags'); - View::share('mainTitleIcon', 'fa-tags'); View::share('hideTags', true); - $this->tagOptions = [ - 'nothing' => trans('firefly.regular_tag'), - 'balancingAct' => trans('firefly.balancing_act'), - 'advancePayment' => trans('firefly.advance_payment'), - ]; - View::share('tagOptions', $this->tagOptions); + + $this->middleware( + function ($request, $next) { + $this->repository = app(TagRepositoryInterface::class); + $this->tagOptions = [ + 'nothing' => trans('firefly.regular_tag'), + 'balancingAct' => trans('firefly.balancing_act'), + 'advancePayment' => trans('firefly.advance_payment'), + ]; + + + View::share('title', strval(trans('firefly.tags'))); + View::share('mainTitleIcon', 'fa-tags'); + View::share('tagOptions', $this->tagOptions); + + + return $next($request); + } + ); } /** + * @param Request $request + * * @return View */ - public function create() + public function create(Request $request) { $subTitle = trans('firefly.new_tag'); $subTitleIcon = 'fa-tag'; + $apiKey = env('GOOGLE_MAPS_API_KEY', ''); $preFilled = [ 'tagMode' => 'nothing', ]; - if (!Input::old('tagMode')) { + if (!$request->old('tagMode')) { Session::flash('preFilled', $preFilled); } // put previous url in session if not redirect from store (not "create another"). if (session('tags.create.fromStore') !== true) { - Session::put('tags.create.url', URL::previous()); + $this->rememberPreviousUri('tags.create.uri'); } Session::forget('tags.create.fromStore'); Session::flash('gaEventCategory', 'tags'); Session::flash('gaEventAction', 'create'); - return view('tags.create', compact('subTitle', 'subTitleIcon')); + return view('tags.create', compact('subTitle', 'subTitleIcon', 'apiKey')); } /** @@ -96,7 +116,7 @@ class TagController extends Controller $subTitle = trans('breadcrumbs.delete_tag', ['tag' => e($tag->tag)]); // put previous url in session - Session::put('tags.delete.url', URL::previous()); + $this->rememberPreviousUri('tags.delete.uri'); Session::flash('gaEventCategory', 'tags'); Session::flash('gaEventAction', 'delete'); @@ -104,21 +124,20 @@ class TagController extends Controller } /** - * @param TagRepositoryInterface $repository - * @param Tag $tag + * @param Tag $tag * * @return \Illuminate\Http\RedirectResponse */ - public function destroy(TagRepositoryInterface $repository, Tag $tag) + public function destroy(Tag $tag) { $tagName = $tag->tag; - $repository->destroy($tag); + $this->repository->destroy($tag); Session::flash('success', strval(trans('firefly.deleted_tag', ['tag' => e($tagName)]))); Preferences::mark(); - return redirect(route('tags.index')); + return redirect($this->getPreviousUri('tags.delete.uri')); } /** @@ -130,6 +149,7 @@ class TagController extends Controller { $subTitle = trans('firefly.edit_tag', ['tag' => $tag->tag]); $subTitleIcon = 'fa-tag'; + $apiKey = env('GOOGLE_MAPS_API_KEY', ''); /* * Default tag options (again) @@ -153,27 +173,13 @@ class TagController extends Controller // put previous url in session if not redirect from store (not "return_to_edit"). if (session('tags.edit.fromUpdate') !== true) { - Session::put('tags.edit.url', URL::previous()); + $this->rememberPreviousUri('tags.edit.uri'); } Session::forget('tags.edit.fromUpdate'); Session::flash('gaEventCategory', 'tags'); Session::flash('gaEventAction', 'edit'); - return view('tags.edit', compact('tag', 'subTitle', 'subTitleIcon', 'tagOptions')); - } - - /** - * @param $state - * - * @return \Symfony\Component\HttpFoundation\Response - */ - public function hideTagHelp(string $state) - { - - $newState = $state == 'true' ? true : false; - Preferences::set('hideTagHelp', $newState); - - return Response::json([true]); + return view('tags.edit', compact('tag', 'subTitle', 'subTitleIcon', 'tagOptions', 'apiKey')); } /** @@ -181,14 +187,9 @@ class TagController extends Controller */ public function index() { - /** @var Preference $helpHiddenPref */ - $helpHiddenPref = Preferences::get('hideTagHelp', false); - $title = 'Tags'; - $mainTitleIcon = 'fa-tags'; - $helpHidden = $helpHiddenPref->data; - - // group years. - $types = ['nothing', 'balancingAct', 'advancePayment']; + $title = 'Tags'; + $mainTitleIcon = 'fa-tags'; + $types = ['nothing', 'balancingAct', 'advancePayment']; // loop each types and get the tags, group them by year. $collection = []; @@ -215,72 +216,97 @@ class TagController extends Controller } } - return view('tags.index', compact('title', 'mainTitleIcon', 'types', 'helpHidden', 'collection')); + return view('tags.index', compact('title', 'mainTitleIcon', 'types', 'collection')); } /** - * @param Tag $tag - * @param TagRepositoryInterface $repository + * @param Request $request + * @param JournalCollectorInterface $collector + * @param Tag $tag * * @return View */ - public function show(Tag $tag, TagRepositoryInterface $repository) + public function show(Request $request, JournalCollectorInterface $collector, Tag $tag, string $moment = '') { + $range = Preferences::get('viewRange', '1M')->data; + $start = new Carbon; + $end = new Carbon; + + if (strlen($moment) > 0) { + try { + $start = new Carbon($moment); + $end = Navigation::endOfPeriod($start, $range); + } catch (Exception $e) { + $start = Navigation::startOfPeriod($this->repository->firstUseDate($tag), $range); + $end = Navigation::startOfPeriod($this->repository->lastUseDate($tag), $range); + } + } + if (strlen($moment) === 0) { + $start = clone session('start', Carbon::now()->startOfMonth()); + $end = clone session('end', Carbon::now()->endOfMonth()); + } + $subTitle = $tag->tag; $subTitleIcon = 'fa-tag'; - $journals = $repository->getJournals($tag); - $sum = $journals->sum( - function (TransactionJournal $journal) { - return TransactionJournal::amount($journal); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $periods = $this->getPeriodOverview($tag); + + // use collector: + $collector->setAllAssetAccounts() + ->setLimit($pageSize)->setPage($page)->setTag($tag) + ->withBudgetInformation()->withCategoryInformation()->setRange($start, $end); + $journals = $collector->getPaginatedJournals(); + $journals->setPath('tags/show/' . $tag->id); + + $sum = $journals->sum( + function (Transaction $transaction) { + return $transaction->transaction_amount; } ); - return view('tags.show', compact('tag', 'subTitle', 'subTitleIcon', 'journals', 'sum')); + return view('tags.show', compact('tag', 'periods', 'subTitle', 'subTitleIcon', 'journals', 'sum', 'start', 'end')); } /** - * @param TagFormRequest $request - * - * @param TagRepositoryInterface $repository + * @param TagFormRequest $request * * @return \Illuminate\Http\RedirectResponse */ - public function store(TagFormRequest $request, TagRepositoryInterface $repository) + public function store(TagFormRequest $request) { $data = $request->collectTagData(); - $repository->store($data); + $this->repository->store($data); Session::flash('success', strval(trans('firefly.created_tag', ['tag' => e($data['tag'])]))); Preferences::mark(); - if (intval(Input::get('create_another')) === 1) { + if (intval($request->get('create_another')) === 1) { // set value so create routine will not overwrite URL: Session::put('tags.create.fromStore', true); return redirect(route('tags.create'))->withInput(); } - // redirect to previous URL. - return redirect(session('tags.create.url')); + return redirect($this->getPreviousUri('tags.create.uri')); } /** - * @param TagFormRequest $request - * @param TagRepositoryInterface $repository - * @param Tag $tag + * @param TagFormRequest $request + * @param Tag $tag * * @return \Illuminate\Http\RedirectResponse */ - public function update(TagFormRequest $request, TagRepositoryInterface $repository, Tag $tag) + public function update(TagFormRequest $request, Tag $tag) { $data = $request->collectTagData(); - $repository->update($tag, $data); + $this->repository->update($tag, $data); Session::flash('success', strval(trans('firefly.updated_tag', ['tag' => e($data['tag'])]))); Preferences::mark(); - if (intval(Input::get('return_to_edit')) === 1) { + if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: Session::put('tags.edit.fromUpdate', true); @@ -288,6 +314,52 @@ class TagController extends Controller } // redirect to previous URL. - return redirect(session('tags.edit.url')); + return redirect($this->getPreviousUri('tags.edit.uri')); + } + + /** + * @param Tag $tag + * + * @return Collection + */ + private function getPeriodOverview(Tag $tag): Collection + { + // get first and last tag date from tag: + $range = Preferences::get('viewRange', '1M')->data; + $start = Navigation::startOfPeriod($this->repository->firstUseDate($tag), $range); + $end = Navigation::startOfPeriod($this->repository->lastUseDate($tag), $range); + // properties for entries with their amounts. + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('tag.entries'); + $cache->addProperty($tag->id); + + if ($cache->has()) { + return $cache->get(); + } + + $collection = new Collection; + + // while end larger or equal to start + while ($end >= $start) { + $currentEnd = Navigation::endOfPeriod($end, $range); + + // get expenses and what-not in this period and this tag. + $arr = [ + 'date_string' => $end->format('Y-m-d'), + 'date_name' => Navigation::periodShow($end, $range), + 'date' => $end, + 'spent' => $this->repository->spentInperiod($tag, $end, $currentEnd), + 'earned' => $this->repository->earnedInperiod($tag, $end, $currentEnd), + ]; + $collection->push($arr); + + $end = Navigation::subtractPeriod($end, $range, 1); + } + $cache->store($collection); + + return $collection; + } } diff --git a/app/Http/Controllers/Transaction/ConvertController.php b/app/Http/Controllers/Transaction/ConvertController.php new file mode 100644 index 0000000000..3fb1e41de7 --- /dev/null +++ b/app/Http/Controllers/Transaction/ConvertController.php @@ -0,0 +1,247 @@ +middleware( + function ($request, $next) { + $this->accounts = app(AccountRepositoryInterface::class); + + View::share('title', trans('firefly.transactions')); + View::share('mainTitleIcon', 'fa-exchange'); + + return $next($request); + } + ); + } + + /** + * @param TransactionType $destinationType + * @param TransactionJournal $journal + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View + */ + public function index(TransactionType $destinationType, TransactionJournal $journal) + { + if ($this->isOpeningBalance($journal)) { + return $this->redirectToAccount($journal); + } + + $positiveAmount = TransactionJournal::amountPositive($journal); + $assetAccounts = ExpandedForm::makeSelectList($this->accounts->getActiveAccountsByType([AccountType::DEFAULT, AccountType::ASSET])); + $sourceType = $journal->transactionType; + $subTitle = trans('firefly.convert_to_' . $destinationType->type, ['description' => $journal->description]); + $subTitleIcon = 'fa-exchange'; + + // cannot convert to its own type. + if ($sourceType->type === $destinationType->type) { + Session::flash('info', trans('firefly.convert_is_already_type_' . $destinationType->type)); + + return redirect(route('transactions.show', [$journal->id])); + } + + // cannot convert split. + if ($journal->transactions()->count() > 2) { + Session::flash('error', trans('firefly.cannot_convert_split_journl')); + + return redirect(route('transactions.show', [$journal->id])); + } + + // get source and destination account: + $sourceAccount = TransactionJournal::sourceAccountList($journal)->first(); + $destinationAccount = TransactionJournal::destinationAccountList($journal)->first(); + + return view( + 'transactions.convert', + compact( + 'sourceType', 'destinationType', 'journal', 'assetAccounts', + 'positiveAmount', 'sourceAccount', 'destinationAccount', 'sourceType', + 'subTitle', 'subTitleIcon' + + ) + ); + + + // convert withdrawal to deposit requires a new source account () + // or to transfer requires + } + + /** + * @param Request $request + * @param JournalRepositoryInterface $repository + * @param TransactionType $destinationType + * @param TransactionJournal $journal + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function postIndex(Request $request, JournalRepositoryInterface $repository, TransactionType $destinationType, TransactionJournal $journal) + { + if ($this->isOpeningBalance($journal)) { + return $this->redirectToAccount($journal); + } + + $data = $request->all(); + + // cannot convert to its own type. + if ($journal->transactionType->type === $destinationType->type) { + Session::flash('error', trans('firefly.convert_is_already_type_' . $destinationType->type)); + + return redirect(route('transactions.show', [$journal->id])); + } + + // cannot convert split. + if ($journal->transactions()->count() > 2) { + Session::flash('error', trans('firefly.cannot_convert_split_journl')); + + return redirect(route('transactions.show', [$journal->id])); + } + + // get the new source and destination account: + $source = $this->getSourceAccount($journal, $destinationType, $data); + $destination = $this->getDestinationAccount($journal, $destinationType, $data); + + // update the journal: + $errors = $repository->convert($journal, $destinationType, $source, $destination); + + if ($errors->count() > 0) { + return redirect(route('transactions.convert.index', [strtolower($destinationType->type), $journal->id]))->withErrors($errors)->withInput(); + } + + Session::flash('success', trans('firefly.converted_to_' . $destinationType->type)); + + return redirect(route('transactions.show', [$journal->id])); + } + + /** + * @param TransactionJournal $journal + * @param TransactionType $destinationType + * @param array $data + * + * @return Account + * @throws FireflyException + */ + private function getDestinationAccount(TransactionJournal $journal, TransactionType $destinationType, array $data): Account + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $sourceAccount = TransactionJournal::sourceAccountList($journal)->first(); + $destinationAccount = TransactionJournal::destinationAccountList($journal)->first(); + $sourceType = $journal->transactionType; + $joined = $sourceType->type . '-' . $destinationType->type; + switch ($joined) { + default: + throw new FireflyException('Cannot handle ' . $joined); + case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: // one + $destination = $sourceAccount; + break; + 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 + $data = [ + 'name' => $data['destination_account_expense'], + 'accountType' => 'expense', + 'virtualBalance' => 0, + 'active' => true, + 'iban' => null, + ]; + $destination = $accountRepository->store($data); + break; + case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: // four + case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: // six + $destination = $destinationAccount; + break; + } + + return $destination; + } + + /** + * @param TransactionJournal $journal + * @param TransactionType $destinationType + * @param array $data + * + * @return Account + * @throws FireflyException + */ + private function getSourceAccount(TransactionJournal $journal, TransactionType $destinationType, array $data): Account + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $sourceAccount = TransactionJournal::sourceAccountList($journal)->first(); + $destinationAccount = TransactionJournal::destinationAccountList($journal)->first(); + $sourceType = $journal->transactionType; + $joined = $sourceType->type . '-' . $destinationType->type; + switch ($joined) { + default: + throw new FireflyException('Cannot handle ' . $joined); + case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: // one + case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: // six + $data = [ + 'name' => $data['source_account_revenue'], + 'accountType' => 'revenue', + 'virtualBalance' => 0, + 'active' => true, + 'iban' => null, + ]; + $source = $accountRepository->store($data); + break; + case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: // two + case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: // five + $source = $sourceAccount; + break; + case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: // three + $source = $destinationAccount; + break; + case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: // four + $source = $accountRepository->find(intval($data['source_account_asset'])); + break; + } + + return $source; + + } + +} diff --git a/app/Http/Controllers/Transaction/MassController.php b/app/Http/Controllers/Transaction/MassController.php index 2d9140ce1d..5a61b060f4 100644 --- a/app/Http/Controllers/Transaction/MassController.php +++ b/app/Http/Controllers/Transaction/MassController.php @@ -25,7 +25,6 @@ use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use Illuminate\Support\Collection; use Preferences; use Session; -use URL; use View; /** @@ -41,8 +40,16 @@ class MassController extends Controller public function __construct() { parent::__construct(); - View::share('title', trans('firefly.transactions')); - View::share('mainTitleIcon', 'fa-repeat'); + + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.transactions')); + View::share('mainTitleIcon', 'fa-repeat'); + + return $next($request); + } + ); } /** @@ -50,12 +57,12 @@ class MassController extends Controller * * @return View */ - public function massDelete(Collection $journals) + public function delete(Collection $journals) { $subTitle = trans('firefly.mass_delete_journals'); // put previous url in session - Session::put('transactions.mass-delete.url', URL::previous()); + $this->rememberPreviousUri('transactions.mass-delete.uri'); Session::flash('gaEventCategory', 'transactions'); Session::flash('gaEventAction', 'mass-delete'); @@ -69,7 +76,7 @@ class MassController extends Controller * * @return mixed */ - public function massDestroy(MassDeleteJournalRequest $request, JournalRepositoryInterface $repository) + public function destroy(MassDeleteJournalRequest $request, JournalRepositoryInterface $repository) { $ids = $request->get('confirm_mass_delete'); $set = new Collection; @@ -96,7 +103,7 @@ class MassController extends Controller Session::flash('success', trans('firefly.mass_deleted_transactions_success', ['amount' => $count])); // redirect to previous URL: - return redirect(session('transactions.mass-delete.url')); + return redirect($this->getPreviousUri('transactions.mass-delete.uri')); } @@ -106,7 +113,7 @@ class MassController extends Controller * * @return View */ - public function massEdit(Collection $journals) + public function edit(Collection $journals) { $subTitle = trans('firefly.mass_edit_journals'); @@ -142,7 +149,7 @@ class MassController extends Controller } // put previous url in session - Session::put('transactions.mass-edit.url', URL::previous()); + $this->rememberPreviousUri('transactions.mass-edit.uri'); Session::flash('gaEventCategory', 'transactions'); Session::flash('gaEventAction', 'mass-edit'); @@ -155,11 +162,11 @@ class MassController extends Controller $journal->transaction_count = $journal->transactions()->count(); if (!is_null($sources->first())) { $journal->source_account_id = $sources->first()->id; - $journal->source_account_name = $sources->first()->name; + $journal->source_account_name = $sources->first()->editname; } if (!is_null($destinations->first())) { $journal->destination_account_id = $destinations->first()->id; - $journal->destination_account_name = $destinations->first()->name; + $journal->destination_account_name = $destinations->first()->editname; } } ); @@ -170,7 +177,7 @@ class MassController extends Controller $journals = $filtered; - return view('transactions.mass-edit', compact('journals', 'subTitle', 'accountList')); + return view('transactions.mass.edit', compact('journals', 'subTitle', 'accountList')); } /** @@ -179,7 +186,7 @@ class MassController extends Controller * * @return mixed */ - public function massUpdate(MassEditJournalRequest $request, JournalRepositoryInterface $repository) + public function update(MassEditJournalRequest $request, JournalRepositoryInterface $repository) { $journalIds = $request->get('journals'); $count = 0; @@ -188,35 +195,33 @@ class MassController extends Controller $journal = $repository->find(intval($journalId)); if ($journal) { // get optional fields: - $what = strtolower(TransactionJournal::transactionTypeStr($journal)); - + $what = strtolower(TransactionJournal::transactionTypeStr($journal)); $sourceAccountId = $request->get('source_account_id')[$journal->id] ?? 0; $sourceAccountName = $request->get('source_account_name')[$journal->id] ?? ''; $destAccountId = $request->get('destination_account_id')[$journal->id] ?? 0; $destAccountName = $request->get('destination_account_name')[$journal->id] ?? ''; - - $budgetId = $journal->budgets->first() ? $journal->budgets->first()->id : 0; - $category = $request->get('category')[$journal->id]; - $tags = $journal->tags->pluck('tag')->toArray(); + $budgetId = $journal->budgets->first() ? $journal->budgets->first()->id : 0; + $category = $request->get('category')[$journal->id]; + $tags = $journal->tags->pluck('tag')->toArray(); // build data array $data = [ - 'id' => $journal->id, - 'what' => $what, - 'description' => $request->get('description')[$journal->id], - 'source_account_id' => intval($sourceAccountId), - 'source_account_name' => $sourceAccountName, - 'destination_account_id' => intval($destAccountId), - 'destination_account_name' => $destAccountName, - 'amount' => round($request->get('amount')[$journal->id], 4), - 'amount_currency_id_amount' => intval($request->get('amount_currency_id_amount_' . $journal->id)), - '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' => $budgetId, - 'category' => $category, - 'tags' => $tags, + 'id' => $journal->id, + 'what' => $what, + 'description' => $request->get('description')[$journal->id], + 'source_account_id' => intval($sourceAccountId), + 'source_account_name' => $sourceAccountName, + 'destination_account_id' => intval($destAccountId), + 'destination_account_name' => $destAccountName, + 'amount' => round($request->get('amount')[$journal->id], 12), + 'currency_id' => intval($request->get('amount_currency_id_amount_' . $journal->id)), + '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' => $budgetId, + 'category' => $category, + 'tags' => $tags, ]; // call repository update function. @@ -230,7 +235,7 @@ class MassController extends Controller Session::flash('success', trans('firefly.mass_edited_transactions_success', ['amount' => $count])); // redirect to previous URL: - return redirect(session('transactions.mass-edit.url')); + return redirect($this->getPreviousUri('transactions.mass-edit.uri')); } } diff --git a/app/Http/Controllers/Transaction/SingleController.php b/app/Http/Controllers/Transaction/SingleController.php index f6e9a4d22c..4a70121724 100644 --- a/app/Http/Controllers/Transaction/SingleController.php +++ b/app/Http/Controllers/Transaction/SingleController.php @@ -31,7 +31,6 @@ use Log; use Preferences; use Session; use Steam; -use URL; use View; /** @@ -59,8 +58,6 @@ class SingleController extends Controller public function __construct() { parent::__construct(); - View::share('title', trans('firefly.transactions')); - View::share('mainTitleIcon', 'fa-repeat'); $maxFileSize = Steam::phpBytes(ini_get('upload_max_filesize')); $maxPostSize = Steam::phpBytes(ini_get('post_max_size')); @@ -75,11 +72,49 @@ class SingleController extends Controller $this->piggyBanks = app(PiggyBankRepositoryInterface::class); $this->attachments = app(AttachmentHelperInterface::class); + View::share('title', trans('firefly.transactions')); + View::share('mainTitleIcon', 'fa-repeat'); + return $next($request); } ); + } + public function cloneTransaction(TransactionJournal $journal) + { + $source = TransactionJournal::sourceAccountList($journal)->first(); + $destination = TransactionJournal::destinationAccountList($journal)->first(); + $budget = $journal->budgets()->first(); + $budgetId = is_null($budget) ? 0 : $budget->id; + $category = $journal->categories()->first(); + $categoryName = is_null($category) ? '' : $category->name; + $tags = join(',', $journal->tags()->get()->pluck('tag')->toArray()); + + + $preFilled = [ + 'description' => $journal->description, + 'source_account_id' => $source->id, + 'source_account_name' => $source->name, + 'destination_account_id' => $destination->id, + 'destination_account_name' => $destination->name, + 'amount' => TransactionJournal::amountPositive($journal), + 'date' => $journal->date->format('Y-m-d'), + 'budget_id' => $budgetId, + 'category' => $categoryName, + 'tags' => $tags, + 'interest_date' => $journal->getMeta('interest_date'), + 'book_date' => $journal->getMeta('book_date'), + 'process_date' => $journal->getMeta('process_date'), + 'due_date' => $journal->getMeta('due_date'), + 'payment_date' => $journal->getMeta('payment_date'), + 'invoice_date' => $journal->getMeta('invoice_date'), + 'internal_reference' => $journal->getMeta('internal_reference'), + 'notes' => $journal->getMeta('notes'), + ]; + Session::flash('preFilled', $preFilled); + + return redirect(route('transactions.create', [strtolower($journal->transactionType->type)])); } /** @@ -104,8 +139,7 @@ class SingleController extends Controller // put previous url in session if not redirect from store (not "create another"). if (session('transactions.create.fromStore') !== true) { - $url = URL::previous(); - Session::put('transactions.create.url', $url); + $this->rememberPreviousUri('transactions.create.uri'); } Session::forget('transactions.create.fromStore'); Session::flash('gaEventCategory', 'transactions'); @@ -113,7 +147,10 @@ class SingleController extends Controller asort($piggies); - return view('transactions.create', compact('assetAccounts', 'subTitleIcon', 'uploadSize', 'budgets', 'what', 'piggies', 'subTitle', 'optionalFields')); + return view( + 'transactions.single.create', + compact('assetAccounts', 'subTitleIcon', 'uploadSize', 'budgets', 'what', 'piggies', 'subTitle', 'optionalFields', 'preFilled') + ); } /** @@ -125,15 +162,19 @@ class SingleController extends Controller */ public function delete(TransactionJournal $journal) { + if ($this->isOpeningBalance($journal)) { + return $this->redirectToAccount($journal); + } + $what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); $subTitle = trans('firefly.delete_' . $what, ['description' => $journal->description]); // put previous url in session - Session::put('transactions.delete.url', URL::previous()); + $this->rememberPreviousUri('transactions.delete.uri'); Session::flash('gaEventCategory', 'transactions'); Session::flash('gaEventAction', 'delete-' . $what); - return view('transactions.delete', compact('journal', 'subTitle', 'what')); + return view('transactions.single.delete', compact('journal', 'subTitle', 'what')); } @@ -146,15 +187,17 @@ class SingleController extends Controller */ public function destroy(JournalRepositoryInterface $repository, TransactionJournal $transactionJournal) { - $type = TransactionJournal::transactionTypeStr($transactionJournal); - Session::flash('success', strval(trans('firefly.deleted_' . $type, ['description' => e($transactionJournal->description)]))); + if ($this->isOpeningBalance($transactionJournal)) { + return $this->redirectToAccount($transactionJournal); + } + $type = TransactionJournal::transactionTypeStr($transactionJournal); + Session::flash('success', strval(trans('firefly.deleted_' . strtolower($type), ['description' => e($transactionJournal->description)]))); $repository->delete($transactionJournal); Preferences::mark(); - // redirect to previous URL: - return redirect(session('transactions.delete.url')); + return redirect($this->getPreviousUri('transactions.delete.uri')); } /** @@ -164,18 +207,23 @@ class SingleController extends Controller */ public function edit(TransactionJournal $journal) { - $count = $journal->transactions()->count(); - if ($count > 2) { - return redirect(route('transactions.edit-split', [$journal->id])); + if ($this->isOpeningBalance($journal)) { + return $this->redirectToAccount($journal); } + $count = $journal->transactions()->count(); + + if ($count > 2) { + return redirect(route('transactions.split.edit', [$journal->id])); + } + + $what = strtolower(TransactionJournal::transactionTypeStr($journal)); $assetAccounts = ExpandedForm::makeSelectList($this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])); - $budgetList = ExpandedForm::makeSelectListWithEmpty($this->budgets->getActiveBudgets()); - $piggyBankList = ExpandedForm::makeSelectListWithEmpty($this->piggyBanks->getPiggyBanks()); + $budgetList = ExpandedForm::makeSelectListWithEmpty($this->budgets->getBudgets()); // view related code $subTitle = trans('breadcrumbs.edit_journal', ['description' => $journal->description]); - $what = strtolower(TransactionJournal::transactionTypeStr($journal)); + // journal related code $sourceAccounts = TransactionJournal::sourceAccountList($journal); @@ -188,12 +236,11 @@ class SingleController extends Controller 'process_date' => TransactionJournal::dateAsString($journal, 'process_date'), 'category' => TransactionJournal::categoryAsString($journal), 'budget_id' => TransactionJournal::budgetId($journal), - 'piggy_bank_id' => TransactionJournal::piggyBankId($journal), 'tags' => join(',', $journal->tags->pluck('tag')->toArray()), 'source_account_id' => $sourceAccounts->first()->id, - 'source_account_name' => $sourceAccounts->first()->name, + 'source_account_name' => $sourceAccounts->first()->edit_name, 'destination_account_id' => $destinationAccounts->first()->id, - 'destination_account_name' => $destinationAccounts->first()->name, + 'destination_account_name' => $destinationAccounts->first()->edit_name, 'amount' => TransactionJournal::amountPositive($journal), // new custom fields: @@ -219,13 +266,13 @@ class SingleController extends Controller // put previous url in session if not redirect from store (not "return_to_edit"). if (session('transactions.edit.fromUpdate') !== true) { - Session::put('transactions.edit.url', URL::previous()); + $this->rememberPreviousUri('transactions.edit.uri'); } Session::forget('transactions.edit.fromUpdate'); return view( - 'transactions.edit', - compact('journal', 'optionalFields', 'assetAccounts', 'what', 'budgetList', 'piggyBankList', 'subTitle') + 'transactions.single.edit', + compact('journal', 'optionalFields', 'assetAccounts', 'what', 'budgetList', 'subTitle') )->with('data', $preFilled); } @@ -248,8 +295,9 @@ class SingleController extends Controller return redirect(route('transactions.create', [$request->input('what')]))->withInput(); } - - $this->attachments->saveAttachmentsForModel($journal); + /** @var array $files */ + $files = $request->hasFile('attachments') ? $request->file('attachments') : null; + $this->attachments->saveAttachmentsForModel($journal, $files); // store the journal only, flash the rest. if (count($this->attachments->getErrors()->get('attachments')) > 0) { @@ -274,12 +322,12 @@ class SingleController extends Controller if ($doSplit === true) { // redirect to edit screen: - return redirect(route('transactions.edit-split', [$journal->id])); + return redirect(route('transactions.split.edit', [$journal->id])); } // redirect to previous URL. - return redirect(session('transactions.create.url')); + return redirect($this->getPreviousUri('transactions.create.uri')); } @@ -292,9 +340,15 @@ class SingleController extends Controller */ public function update(JournalFormRequest $request, JournalRepositoryInterface $repository, TransactionJournal $journal) { + if ($this->isOpeningBalance($journal)) { + return $this->redirectToAccount($journal); + } + $data = $request->getJournalData(); $journal = $repository->update($journal, $data); - $this->attachments->saveAttachmentsForModel($journal); + /** @var array $files */ + $files = $request->hasFile('attachments') ? $request->file('attachments') : null; + $this->attachments->saveAttachmentsForModel($journal, $files); // flash errors if (count($this->attachments->getErrors()->get('attachments')) > 0) { @@ -323,9 +377,6 @@ class SingleController extends Controller } // redirect to previous URL. - return redirect(session('transactions.edit.url')); - + return redirect($this->getPreviousUri('transactions.edit.uri')); } - - } diff --git a/app/Http/Controllers/Transaction/SplitController.php b/app/Http/Controllers/Transaction/SplitController.php index 2247c30fd3..e667cb5f1b 100644 --- a/app/Http/Controllers/Transaction/SplitController.php +++ b/app/Http/Controllers/Transaction/SplitController.php @@ -30,7 +30,6 @@ use Log; use Preferences; use Session; use Steam; -use URL; use View; /** @@ -63,8 +62,7 @@ class SplitController extends Controller public function __construct() { parent::__construct(); - View::share('mainTitleIcon', 'fa-share-alt'); - View::share('title', trans('firefly.split-transactions')); + // some useful repositories: $this->middleware( @@ -74,6 +72,8 @@ class SplitController extends Controller $this->tasker = app(JournalTaskerInterface::class); $this->attachments = app(AttachmentHelperInterface::class); $this->currencies = app(CurrencyRepositoryInterface::class); + View::share('mainTitleIcon', 'fa-share-alt'); + View::share('title', trans('firefly.split-transactions')); return $next($request); } @@ -88,6 +88,10 @@ class SplitController extends Controller */ public function edit(Request $request, TransactionJournal $journal) { + if ($this->isOpeningBalance($journal)) { + return $this->redirectToAccount($journal); + } + $uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size'))); $currencies = ExpandedForm::makeSelectList($this->currencies->get()); $assetAccounts = ExpandedForm::makeSelectList($this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])); @@ -102,12 +106,12 @@ class SplitController extends Controller // put previous url in session if not redirect from store (not "return_to_edit"). if (session('transactions.edit-split.fromUpdate') !== true) { - Session::put('transactions.edit-split.url', URL::previous()); + $this->rememberPreviousUri('transactions.edit-split.uri'); } Session::forget('transactions.edit-split.fromUpdate'); return view( - 'transactions.edit-split', + 'transactions.split.edit', compact( 'subTitleIcon', 'currencies', 'optionalFields', 'preFilled', 'subTitle', 'amount', 'sourceAccounts', 'uploadSize', 'destinationAccounts', 'assetAccounts', @@ -126,11 +130,17 @@ class SplitController extends Controller */ public function update(Request $request, JournalRepositoryInterface $repository, TransactionJournal $journal) { + + if ($this->isOpeningBalance($journal)) { + return $this->redirectToAccount($journal); + } + $data = $this->arrayFromInput($request); $journal = $repository->updateSplitJournal($journal, $data); - + /** @var array $files */ + $files = $request->hasFile('attachments') ? $request->file('attachments') : null; // save attachments: - $this->attachments->saveAttachmentsForModel($journal); + $this->attachments->saveAttachmentsForModel($journal, $files); event(new UpdatedTransactionJournal($journal)); // update, get events by date and sort DESC @@ -148,12 +158,11 @@ class SplitController extends Controller // set value so edit routine will not overwrite URL: Session::put('transactions.edit-split.fromUpdate', true); - return redirect(route('transactions.edit-split', [$journal->id]))->withInput(['return_to_edit' => 1]); + return redirect(route('transactions.split.edit', [$journal->id]))->withInput(['return_to_edit' => 1]); } // redirect to previous URL. - return redirect(session('transactions.edit-split.url')); - + return redirect($this->getPreviousUri('transactions.edit-split.uri')); } /** @@ -186,6 +195,7 @@ class SplitController extends Controller 'transactions' => $this->getTransactionDataFromRequest($request), ]; + return $array; } @@ -239,17 +249,26 @@ class SplitController extends Controller $transactions = $this->tasker->getTransactionsOverview($journal); $return = []; /** @var array $transaction */ - foreach ($transactions as $transaction) { - $return[] = [ + 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'], 2), + 'amount' => round($transaction['destination_amount'], 12), 'budget_id' => isset($transaction['budget_id']) ? intval($transaction['budget_id']) : 0, 'category' => $transaction['category'], ]; + + // set initial category and/or budget: + if (count($transactions) === 1 && $index === 0) { + $set['budget_id'] = TransactionJournal::budgetId($journal); + $set['category'] = TransactionJournal::categoryAsString($journal); + } + + $return[] = $set; + } return $return; @@ -272,7 +291,7 @@ class SplitController extends Controller 'source_account_name' => $transaction['source_account_name'] ?? '', 'destination_account_id' => $transaction['destination_account_id'] ?? 0, 'destination_account_name' => $transaction['destination_account_name'] ?? '', - 'amount' => round($transaction['amount'] ?? 0, 2), + 'amount' => round($transaction['amount'] ?? 0, 12), 'budget_id' => isset($transaction['budget_id']) ? intval($transaction['budget_id']) : 0, 'category' => $transaction['category'] ?? '', ]; @@ -282,4 +301,5 @@ class SplitController extends Controller return $return; } + } diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index 8cd732a617..b0ef6013ab 100644 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -14,10 +14,14 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers; use Carbon\Carbon; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Journal\JournalTaskerInterface; use Illuminate\Http\Request; +use Illuminate\Support\Collection; +use Log; +use Navigation; use Preferences; use Response; use View; @@ -35,30 +39,130 @@ class TransactionController extends Controller public function __construct() { parent::__construct(); - View::share('title', trans('firefly.transactions')); - View::share('mainTitleIcon', 'fa-repeat'); + + + $this->middleware( + function ($request, $next) { + View::share('title', trans('firefly.transactions')); + View::share('mainTitleIcon', 'fa-repeat'); + + return $next($request); + } + ); } /** - * @param Request $request - * @param JournalTaskerInterface $tasker - * @param string $what + * @param Request $request + * @param JournalRepositoryInterface $repository + * @param string $what * * @return View */ - public function index(Request $request, JournalTaskerInterface $tasker, string $what) + public function index(Request $request, JournalRepositoryInterface $repository, string $what) { $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $subTitleIcon = config('firefly.transactionIconsByWhat.' . $what); $types = config('firefly.transactionTypesByWhat.' . $what); $subTitle = trans('firefly.title_' . $what); - $page = intval($request->get('page')); - $journals = $tasker->getJournals($types, $page, $pageSize); + $range = Preferences::get('viewRange', '1M')->data; + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + // to make sure we only grab a subset, based on the current date (in session): + $start = session('start', Navigation::startOfPeriod(new Carbon, $range)); + $end = session('end', Navigation::endOfPeriod(new Carbon, $range)); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setTypes($types)->setLimit($pageSize)->setPage($page)->setAllAssetAccounts()->setRange($start, $end)->withBudgetInformation() + ->withCategoryInformation(); + $collector->withOpposingAccount(); + $collector->disableInternalFilter(); + $journals = $collector->getPaginatedJournals(); $journals->setPath('transactions/' . $what); - return view('transactions.index', compact('subTitle', 'what', 'subTitleIcon', 'journals')); + unset($start, $end); + + // then also show a list of periods where the user can click on, based on the + // user's range and the oldest journal the user has: + $first = $repository->first(); + $blockStart = is_null($first->id) ? new Carbon : $first->date; + $blockStart = Navigation::startOfPeriod($blockStart, $range); + $blockEnd = Navigation::endOfX(new Carbon, $range); + $entries = new Collection; + + while ($blockEnd >= $blockStart) { + Log::debug(sprintf('Now at blockEnd: %s', $blockEnd->format('Y-m-d'))); + $blockEnd = Navigation::startOfPeriod($blockEnd, $range); + $dateStr = $blockEnd->format('Y-m-d'); + $dateName = Navigation::periodShow($blockEnd, $range); + $entries->push([$dateStr, $dateName]); + $blockEnd = Navigation::subtractPeriod($blockEnd, $range, 1); + } + + return view('transactions.index', compact('subTitle', 'what', 'subTitleIcon', 'journals', 'entries')); + + } + + /** + * @param Request $request + * @param string $what + * + * @return View + */ + public function indexAll(Request $request, string $what) + { + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $subTitleIcon = config('firefly.transactionIconsByWhat.' . $what); + $types = config('firefly.transactionTypesByWhat.' . $what); + $subTitle = sprintf('%s (%s)', trans('firefly.title_' . $what), strtolower(trans('firefly.everything'))); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setTypes($types)->setLimit($pageSize)->setPage($page)->setAllAssetAccounts()->withBudgetInformation()->withCategoryInformation(); + $collector->withOpposingAccount(); + $collector->disableInternalFilter(); + + $journals = $collector->getPaginatedJournals(); + $journals->setPath('transactions/' . $what . '/all'); + + return view('transactions.index-all', compact('subTitle', 'what', 'subTitleIcon', 'journals')); + + } + + /** + * @param Request $request + * @param string $what + * + * @param string $date + * + * @return View + */ + public function indexByDate(Request $request, string $what, string $date) + { + $carbon = new Carbon($date); + $range = Preferences::get('viewRange', '1M')->data; + $start = Navigation::startOfPeriod($carbon, $range); + $end = Navigation::endOfPeriod($carbon, $range); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $subTitleIcon = config('firefly.transactionIconsByWhat.' . $what); + $types = config('firefly.transactionTypesByWhat.' . $what); + $subTitle = trans('firefly.title_' . $what) . ' (' . Navigation::periodShow($carbon, $range) . ')'; + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + + Log::debug(sprintf('Transaction index by date will show between %s and %s', $start->format('Y-m-d'), $end->format('Y-m-d'))); + + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser(auth()->user()); + $collector->setTypes($types)->setLimit($pageSize)->setPage($page)->setAllAssetAccounts(); + $collector->setRange($start, $end)->withBudgetInformation()->withCategoryInformation(); + $collector->withOpposingAccount(); + $collector->disableInternalFilter(); + $journals = $collector->getPaginatedJournals(); + $journals->setPath('transactions/' . $what . '/' . $date); + + return view('transactions.index-date', compact('subTitle', 'what', 'subTitleIcon', 'journals', 'carbon')); } @@ -98,6 +202,10 @@ class TransactionController extends Controller */ public function show(TransactionJournal $journal, JournalTaskerInterface $tasker) { + if ($this->isOpeningBalance($journal)) { + return $this->redirectToAccount($journal); + } + $events = $tasker->getPiggyBankEvents($journal); $transactions = $tasker->getTransactionsOverview($journal); $what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); @@ -107,4 +215,5 @@ class TransactionController extends Controller } + } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php old mode 100755 new mode 100644 index b9f1190f10..289a6924d1 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -17,11 +17,11 @@ use FireflyIII\Http\Middleware\AuthenticateTwoFactor; use FireflyIII\Http\Middleware\Binder; use FireflyIII\Http\Middleware\EncryptCookies; use FireflyIII\Http\Middleware\IsAdmin; -use FireflyIII\Http\Middleware\IsConfirmed; -use FireflyIII\Http\Middleware\IsNotConfirmed; use FireflyIII\Http\Middleware\Range; use FireflyIII\Http\Middleware\RedirectIfAuthenticated; use FireflyIII\Http\Middleware\RedirectIfTwoFactorAuthenticated; +use FireflyIII\Http\Middleware\Sandstorm; +use FireflyIII\Http\Middleware\StartFireflySession; use FireflyIII\Http\Middleware\VerifyCsrfToken; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Illuminate\Auth\Middleware\Authorize; @@ -30,7 +30,6 @@ use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Routing\Middleware\ThrottleRequests; -use Illuminate\Session\Middleware\StartSession; use Illuminate\View\Middleware\ShareErrorsFromSession; /** @@ -50,9 +49,8 @@ class Kernel extends HttpKernel */ protected $bootstrappers = [ - 'Illuminate\Foundation\Bootstrap\DetectEnvironment', + 'Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables', 'Illuminate\Foundation\Bootstrap\LoadConfiguration', - 'FireflyIII\Bootstrap\ConfigureLogging', 'Illuminate\Foundation\Bootstrap\HandleExceptions', 'Illuminate\Foundation\Bootstrap\RegisterFacades', 'Illuminate\Foundation\Bootstrap\RegisterProviders', @@ -81,19 +79,23 @@ class Kernel extends HttpKernel // does not check login // does not check 2fa // does not check activation - 'web' => [ + 'web' => [ + Sandstorm::class, EncryptCookies::class, AddQueuedCookiesToResponse::class, - StartSession::class, + StartFireflySession::class, ShareErrorsFromSession::class, VerifyCsrfToken::class, SubstituteBindings::class, ], + + // MUST NOT be logged in. Does not care about 2FA or confirmation. - 'user-not-logged-in' => [ + 'user-not-logged-in' => [ + Sandstorm::class, EncryptCookies::class, AddQueuedCookiesToResponse::class, - StartSession::class, + StartFireflySession::class, ShareErrorsFromSession::class, VerifyCsrfToken::class, SubstituteBindings::class, @@ -102,38 +104,26 @@ class Kernel extends HttpKernel // MUST be logged in. // MUST NOT have 2FA // don't care about confirmation: - 'user-logged-in-no-2fa' => [ + 'user-logged-in-no-2fa' => [ + Sandstorm::class, EncryptCookies::class, AddQueuedCookiesToResponse::class, - StartSession::class, + StartFireflySession::class, ShareErrorsFromSession::class, VerifyCsrfToken::class, SubstituteBindings::class, Authenticate::class, RedirectIfTwoFactorAuthenticated::class, ], - // MUST be logged in - // MUST have 2FA - // MUST NOT have confirmation. - 'user-logged-in-2fa-no-activation' => [ - EncryptCookies::class, - AddQueuedCookiesToResponse::class, - StartSession::class, - ShareErrorsFromSession::class, - VerifyCsrfToken::class, - SubstituteBindings::class, - Authenticate::class, - AuthenticateTwoFactor::class, - IsNotConfirmed::class, - ], // MUST be logged in // don't care about 2fa // don't care about confirmation. - 'user-simple-auth' => [ + 'user-simple-auth' => [ + Sandstorm::class, EncryptCookies::class, AddQueuedCookiesToResponse::class, - StartSession::class, + StartFireflySession::class, ShareErrorsFromSession::class, VerifyCsrfToken::class, SubstituteBindings::class, @@ -144,16 +134,16 @@ class Kernel extends HttpKernel // MUST have 2fa // MUST be confirmed. // (this group includes the other Firefly middleware) - 'user-full-auth' => [ + 'user-full-auth' => [ + Sandstorm::class, EncryptCookies::class, AddQueuedCookiesToResponse::class, - StartSession::class, + StartFireflySession::class, ShareErrorsFromSession::class, VerifyCsrfToken::class, SubstituteBindings::class, Authenticate::class, AuthenticateTwoFactor::class, - IsConfirmed::class, Range::class, Binder::class, ], @@ -162,20 +152,19 @@ class Kernel extends HttpKernel // MUST be confirmed. // MUST have owner role // (this group includes the other Firefly middleware) - 'admin' => [ + 'admin' => [ + Sandstorm::class, EncryptCookies::class, AddQueuedCookiesToResponse::class, - StartSession::class, + StartFireflySession::class, ShareErrorsFromSession::class, VerifyCsrfToken::class, SubstituteBindings::class, Authenticate::class, AuthenticateTwoFactor::class, - IsConfirmed::class, IsAdmin::class, Range::class, Binder::class, - ], diff --git a/app/Http/Middleware/AuthenticateTwoFactor.php b/app/Http/Middleware/AuthenticateTwoFactor.php index 7dbaa77505..4f9f1f5542 100644 --- a/app/Http/Middleware/AuthenticateTwoFactor.php +++ b/app/Http/Middleware/AuthenticateTwoFactor.php @@ -57,7 +57,7 @@ class AuthenticateTwoFactor $has2faSecret = !is_null(Preferences::get('twoFactorAuthSecret')); $is2faAuthed = Session::get('twofactor-authenticated'); if ($is2faEnabled && $has2faSecret && !$is2faAuthed) { - return redirect(route('two-factor')); + return redirect(route('two-factor.index')); } return $next($request); diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php old mode 100755 new mode 100644 diff --git a/app/Http/Middleware/IsAdmin.php b/app/Http/Middleware/IsAdmin.php index 598de92d86..823be2f20d 100644 --- a/app/Http/Middleware/IsAdmin.php +++ b/app/Http/Middleware/IsAdmin.php @@ -26,8 +26,7 @@ use Illuminate\Support\Facades\Auth; class IsAdmin { /** - * Handle an incoming request. User account must be confirmed for this routine to let - * the user pass. + * Handle an incoming request. Must be admin. * * @param \Illuminate\Http\Request $request * @param \Closure $next diff --git a/app/Http/Middleware/IsConfirmed.php b/app/Http/Middleware/IsConfirmed.php deleted file mode 100644 index bd6dfa1c02..0000000000 --- a/app/Http/Middleware/IsConfirmed.php +++ /dev/null @@ -1,61 +0,0 @@ -guest()) { - if ($request->ajax()) { - return response('Unauthorized.', 401); - } - - return redirect()->guest('login'); - } - // must the user be confirmed in the first place? - $confirmAccount = env('MUST_CONFIRM_ACCOUNT', false); - // user must be logged in, then continue: - $isConfirmed = Preferences::get('user_confirmed', false)->data; - - if ($isConfirmed === false && $confirmAccount === true) { - - // user account is not confirmed, redirect to - // confirmation page: - return redirect(route('confirmation_error')); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/IsNotConfirmed.php b/app/Http/Middleware/IsNotConfirmed.php deleted file mode 100644 index 62887408c0..0000000000 --- a/app/Http/Middleware/IsNotConfirmed.php +++ /dev/null @@ -1,58 +0,0 @@ -guest()) { - if ($request->ajax()) { - return response('Unauthorized.', 401); - } - - return redirect()->guest('login'); - } - // must the user be confirmed in the first place? - $confirmAccount = env('MUST_CONFIRM_ACCOUNT', false); - // user must be logged in, then continue: - $isConfirmed = Preferences::get('user_confirmed', false)->data; - if ($isConfirmed || $confirmAccount === false) { - // user account is confirmed, simply send them home. - return redirect(route('home')); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/Range.php b/app/Http/Middleware/Range.php index c483088515..fdf5322a4d 100644 --- a/app/Http/Middleware/Range.php +++ b/app/Http/Middleware/Range.php @@ -13,6 +13,7 @@ declare(strict_types = 1); namespace FireflyIII\Http\Middleware; +use Amount; use App; use Carbon\Carbon; use Closure; @@ -22,7 +23,6 @@ use Illuminate\Contracts\Auth\Guard; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Navigation; -use NumberFormatter; use Preferences; use Session; use View; @@ -57,22 +57,18 @@ class Range * Handle an incoming request. * * @param \Illuminate\Http\Request $request - * @param Closure $theNext + * @param Closure $next * @param string|null $guard * * @return mixed - * @internal param Closure $next */ - public function handle(Request $request, Closure $theNext, $guard = null) + public function handle(Request $request, Closure $next, $guard = null) { if (!Auth::guard($guard)->guest()) { // set start, end and finish: $this->setRange(); - // get variables for date range: - $this->datePicker(); - // set view variables. $this->configureView(); @@ -80,7 +76,7 @@ class Range $this->configureList(); } - return $theNext($request); + return $next($request); } @@ -97,7 +93,6 @@ class Range { $pref = Preferences::get('language', config('firefly.default_language', 'en_US')); $lang = $pref->data; - App::setLocale($lang); Carbon::setLocale(substr($lang, 0, 2)); $locale = explode(',', trans('config.locale')); @@ -106,75 +101,15 @@ class Range setlocale(LC_TIME, $locale); setlocale(LC_MONETARY, $locale); + // save some formats: - $monthFormat = (string)trans('config.month'); $monthAndDayFormat = (string)trans('config.month_and_day'); $dateTimeFormat = (string)trans('config.date_time'); + $defaultCurrency = Amount::getDefaultCurrency(); - // change localeconv to a new array: - $numberFormatter = numfmt_create($lang, NumberFormatter::CURRENCY); - $localeconv = [ - 'mon_decimal_point' => $numberFormatter->getSymbol($numberFormatter->getAttribute(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL)), - 'mon_thousands_sep' => $numberFormatter->getSymbol($numberFormatter->getAttribute(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL)), - 'frac_digits' => $numberFormatter->getAttribute(NumberFormatter::MAX_FRACTION_DIGITS), - ]; - View::share('monthFormat', $monthFormat); View::share('monthAndDayFormat', $monthAndDayFormat); View::share('dateTimeFormat', $dateTimeFormat); - View::share('language', $lang); - View::share('localeconv', $localeconv); - } - - /** - * @throws FireflyException - */ - private function datePicker() - { - $viewRange = Preferences::get('viewRange', '1M')->data; - $start = Session::get('start'); - $end = Session::get('end'); - $prevStart = Navigation::subtractPeriod($start, $viewRange);// subtract for previous period - $prevEnd = Navigation::endOfPeriod($prevStart, $viewRange); - $nextStart = Navigation::addPeriod($start, $viewRange, 0); // add for previous period - $nextEnd = Navigation::endOfPeriod($nextStart, $viewRange); - $ranges = []; - $ranges['current'] = [$start->format('Y-m-d'), $end->format('Y-m-d')]; - $ranges['previous'] = [$prevStart->format('Y-m-d'), $prevEnd->format('Y-m-d')]; - $ranges['next'] = [$nextStart->format('Y-m-d'), $nextEnd->format('Y-m-d')]; - - switch ($viewRange) { - default: - throw new FireflyException('The date picker does not yet support "' . $viewRange . '".'); - case '1D': - $format = (string)trans('config.month_and_day'); - break; - case '3M': - $format = (string)trans('config.quarter_in_year'); - break; - case '6M': - $format = (string)trans('config.half_year'); - break; - case '1Y': - $format = (string)trans('config.year'); - break; - case '1M': - $format = (string)trans('config.month'); - break; - case '1W': - $format = (string)trans('config.week_in_year'); - break; - } - - - $current = $start->formatLocalized($format); - $next = $nextStart->formatLocalized($format); - $prev = $prevStart->formatLocalized($format); - View::share('dpStart', $start->format('Y-m-d')); - View::share('dpEnd', $end->format('Y-m-d')); - View::share('dpCurrent', $current); - View::share('dpPrevious', $prev); - View::share('dpNext', $next); - View::share('dpRanges', $ranges); + View::share('defaultCurrency', $defaultCurrency); } /** diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php old mode 100755 new mode 100644 diff --git a/app/Http/Middleware/Sandstorm.php b/app/Http/Middleware/Sandstorm.php new file mode 100644 index 0000000000..92225ceef5 --- /dev/null +++ b/app/Http/Middleware/Sandstorm.php @@ -0,0 +1,75 @@ +guest()) { + $userId = strval($request->header('X-Sandstorm-User-Id')); + if (strlen($userId) > 0) { + // find user? + $email = $userId . '@firefly'; + $user = User::whereEmail($email)->first(); + if (is_null($user)) { + $user = User::create( + [ + 'email' => $email, + 'password' => str_random(16), + ] + ); + + } + + + // login user: + Auth::guard($guard)->login($user); + } else { + echo 'user id no length, guest?'; + exit; + } + + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/StartFireflySession.php b/app/Http/Middleware/StartFireflySession.php new file mode 100644 index 0000000000..ba6c518a08 --- /dev/null +++ b/app/Http/Middleware/StartFireflySession.php @@ -0,0 +1,55 @@ +fullUrl(); + if ($request->method() === 'GET' && $request->route() && !$request->ajax()) { + if (strpos($fullUrl, '/javascript/') === false) { + $session->setPreviousUrl($fullUrl); + } + } + } + +} diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php old mode 100755 new mode 100644 index 6eb1050494..11ec0455c9 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -13,7 +13,8 @@ declare(strict_types = 1); namespace FireflyIII\Http\Middleware; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier; - +use Symfony\Component\HttpFoundation\Cookie; +use Carbon\Carbon; /** * Class VerifyCsrfToken * @@ -30,4 +31,25 @@ class VerifyCsrfToken extends BaseVerifier = [ // ]; + + /** + * Add the CSRF token to the response cookies. + * + * @param \Illuminate\Http\Request $request + * @param \Symfony\Component\HttpFoundation\Response $response + * @return \Symfony\Component\HttpFoundation\Response + */ + protected function addCookieToResponse($request, $response) + { + $config = config('session'); + + $response->headers->setCookie( + new Cookie( + 'XSRF-TOKEN', $request->session()->token(), Carbon::now()->getTimestamp() + 60 * $config['lifetime'], + $config['path'], $config['domain'], $config['secure'], true + ) + ); + + return $response; + } } diff --git a/app/Http/Requests/AccountFormRequest.php b/app/Http/Requests/AccountFormRequest.php index b40e21785f..cadf22b600 100644 --- a/app/Http/Requests/AccountFormRequest.php +++ b/app/Http/Requests/AccountFormRequest.php @@ -13,7 +13,6 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; -use Carbon\Carbon; use FireflyIII\Repositories\Account\AccountRepositoryInterface; /** @@ -39,19 +38,21 @@ class AccountFormRequest extends Request public function getAccountData(): array { return [ - 'name' => trim($this->input('name')), - 'active' => intval($this->input('active')) === 1, - 'accountType' => $this->input('what'), - 'virtualBalance' => round($this->input('virtualBalance'), 2), - 'virtualBalanceCurrency' => intval($this->input('amount_currency_id_virtualBalance')), - 'iban' => trim($this->input('iban')), - 'accountNumber' => trim($this->input('accountNumber')), - 'accountRole' => $this->input('accountRole'), - 'openingBalance' => round($this->input('openingBalance'), 2), - 'openingBalanceDate' => new Carbon((string)$this->input('openingBalanceDate')), - 'openingBalanceCurrency' => intval($this->input('amount_currency_id_openingBalance')), - 'ccType' => $this->input('ccType'), - 'ccMonthlyPaymentDate' => $this->input('ccMonthlyPaymentDate'), + 'name' => $this->string('name'), + 'active' => $this->boolean('active'), + 'accountType' => $this->string('what'), + 'currency_id' => $this->integer('currency_id'), + 'virtualBalance' => $this->float('virtualBalance'), + 'virtualBalanceCurrency' => $this->integer('amount_currency_id_virtualBalance'), + 'iban' => $this->string('iban'), + 'BIC' => $this->string('BIC'), + 'accountNumber' => $this->string('accountNumber'), + 'accountRole' => $this->string('accountRole'), + 'openingBalance' => $this->float('openingBalance'), + 'openingBalanceDate' => $this->date('openingBalanceDate'), + 'openingBalanceCurrency' => $this->integer('amount_currency_id_openingBalance'), + 'ccType' => $this->string('ccType'), + 'ccMonthlyPaymentDate' => $this->string('ccMonthlyPaymentDate'), ]; } @@ -62,7 +63,7 @@ class AccountFormRequest extends Request { /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); - $accountRoles = join(',', array_keys(config('firefly.accountRoles'))); + $accountRoles = join(',', config('firefly.accountRoles')); $types = join(',', array_keys(config('firefly.subTitlesByIdentifier'))); $ccPaymentTypes = join(',', array_keys(config('firefly.ccTypes'))); @@ -70,16 +71,18 @@ class AccountFormRequest extends Request $idRule = ''; if (!is_null($repository->find(intval($this->get('id')))->id)) { $idRule = 'belongsToUser:accounts'; - $nameRule = 'required|min:1|uniqueAccountForUser:' . $this->get('id'); + $nameRule = 'required|min:1|uniqueAccountForUser:' . intval($this->get('id')); } return [ 'id' => $idRule, 'name' => $nameRule, - 'openingBalance' => 'numeric', + 'openingBalance' => 'numeric|required_with:openingBalanceDate', + 'openingBalanceDate' => 'date|required_with:openingBalance', 'iban' => 'iban', + 'BIC' => 'bic', 'virtualBalance' => 'numeric', - 'openingBalanceDate' => 'date', + 'currency_id' => 'exists:transaction_currencies,id', 'accountNumber' => 'between:1,255|uniqueAccountNumberForUser', 'accountRole' => 'in:' . $accountRoles, 'active' => 'boolean', diff --git a/app/Http/Requests/AttachmentFormRequest.php b/app/Http/Requests/AttachmentFormRequest.php index 16028cab37..6f7608bc46 100644 --- a/app/Http/Requests/AttachmentFormRequest.php +++ b/app/Http/Requests/AttachmentFormRequest.php @@ -36,9 +36,9 @@ class AttachmentFormRequest extends Request public function getAttachmentData(): array { return [ - 'title' => trim($this->input('title')), - 'description' => trim($this->input('description')), - 'notes' => trim($this->input('notes')), + 'title' => $this->string('title'), + 'description' => $this->string('description'), + 'notes' => $this->string('notes'), ]; } diff --git a/app/Http/Requests/BillFormRequest.php b/app/Http/Requests/BillFormRequest.php index b16ce0ec33..992542eca1 100644 --- a/app/Http/Requests/BillFormRequest.php +++ b/app/Http/Requests/BillFormRequest.php @@ -13,8 +13,6 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; -use Carbon\Carbon; - /** * Class BillFormRequest * @@ -38,17 +36,17 @@ class BillFormRequest extends Request public function getBillData() { return [ - 'name' => $this->get('name'), - 'match' => $this->get('match'), - 'amount_min' => round($this->get('amount_min'), 2), - 'amount_currency_id_amount_min' => intval($this->get('amount_currency_id_amount_min')), - 'amount_currency_id_amount_max' => intval($this->get('amount_currency_id_amount_max')), - 'amount_max' => round($this->get('amount_max'), 2), - 'date' => new Carbon($this->get('date')), - 'repeat_freq' => $this->get('repeat_freq'), - 'skip' => intval($this->get('skip')), - 'automatch' => intval($this->get('automatch')) === 1, - 'active' => intval($this->get('active')) === 1, + 'name' => $this->string('name'), + 'match' => $this->string('match'), + 'amount_min' => $this->float('amount_min'), + 'amount_currency_id_amount_min' => $this->integer('amount_currency_id_amount_min'), + 'amount_currency_id_amount_max' => $this->integer('amount_currency_id_amount_max'), + 'amount_max' => $this->float('amount_max'), + 'date' => $this->date('date'), + 'repeat_freq' => $this->string('repeat_freq'), + 'skip' => $this->integer('skip'), + 'automatch' => $this->boolean('automatch'), + 'active' => $this->boolean('active'), ]; } @@ -67,8 +65,8 @@ class BillFormRequest extends Request $rules = [ 'name' => $nameRule, 'match' => $matchRule, - 'amount_min' => 'required|numeric|min:0.01', - 'amount_max' => 'required|numeric|min:0.01', + 'amount_min' => 'required|numeric|more:0', + 'amount_max' => 'required|numeric|more:0', 'amount_currency_id_amount_min' => 'required|exists:transaction_currencies,id', 'amount_currency_id_amount_max' => 'required|exists:transaction_currencies,id', 'date' => 'required|date', diff --git a/app/Http/Requests/BudgetFormRequest.php b/app/Http/Requests/BudgetFormRequest.php index 3ca9d0fb37..c7d9d0de63 100644 --- a/app/Http/Requests/BudgetFormRequest.php +++ b/app/Http/Requests/BudgetFormRequest.php @@ -37,8 +37,8 @@ class BudgetFormRequest extends Request public function getBudgetData(): array { return [ - 'name' => trim($this->input('name')), - 'active' => intval($this->input('active')) == 1, + 'name' => $this->string('name'), + 'active' => $this->boolean('active'), ]; } diff --git a/app/Http/Requests/BudgetIncomeRequest.php b/app/Http/Requests/BudgetIncomeRequest.php new file mode 100644 index 0000000000..e8c2c93e39 --- /dev/null +++ b/app/Http/Requests/BudgetIncomeRequest.php @@ -0,0 +1,42 @@ +check(); + } + + /** + * @return array + */ + public function rules() + { + return [ + 'amount' => 'numeric|required|min:0', + ]; + } +} diff --git a/app/Http/Requests/CategoryFormRequest.php b/app/Http/Requests/CategoryFormRequest.php index 34347608af..b7b5e94a5e 100644 --- a/app/Http/Requests/CategoryFormRequest.php +++ b/app/Http/Requests/CategoryFormRequest.php @@ -38,7 +38,7 @@ class CategoryFormRequest extends Request public function getCategoryData(): array { return [ - 'name' => trim($this->input('name')), + 'name' => $this->string('name'), ]; } diff --git a/app/Http/Requests/ConfigurationRequest.php b/app/Http/Requests/ConfigurationRequest.php index 2d9cc6a9a9..452e613e73 100644 --- a/app/Http/Requests/ConfigurationRequest.php +++ b/app/Http/Requests/ConfigurationRequest.php @@ -36,7 +36,8 @@ class ConfigurationRequest extends Request public function getConfigurationData(): array { return [ - 'single_user_mode' => intval($this->get('single_user_mode')) === 1, + 'single_user_mode' => $this->boolean('single_user_mode'), + 'is_demo_site' => $this->boolean('is_demo_site'), ]; } @@ -47,6 +48,7 @@ class ConfigurationRequest extends Request { $rules = [ 'single_user_mode' => 'between:0,1|numeric', + 'is_demo_site' => 'between:0,1|numeric', ]; return $rules; diff --git a/app/Http/Requests/CurrencyFormRequest.php b/app/Http/Requests/CurrencyFormRequest.php index 74b805be84..0449f9b4ab 100644 --- a/app/Http/Requests/CurrencyFormRequest.php +++ b/app/Http/Requests/CurrencyFormRequest.php @@ -36,9 +36,10 @@ class CurrencyFormRequest extends Request public function getCurrencyData() { return [ - 'name' => $this->get('name'), - 'code' => $this->get('code'), - 'symbol' => $this->get('symbol'), + 'name' => $this->string('name'), + 'code' => $this->string('code'), + 'symbol' => $this->string('symbol'), + 'decimal_places' => $this->integer('decimal_places'), ]; } @@ -49,15 +50,17 @@ class CurrencyFormRequest extends Request { $rules = [ - 'code' => 'required|min:3|max:3|unique:transaction_currencies,code', - 'name' => 'required|max:48|min:1|unique:transaction_currencies,name', - 'symbol' => 'required|min:1|max:8|unique:transaction_currencies,symbol', + 'name' => 'required|max:48|min:1|unique:transaction_currencies,name', + 'code' => 'required|min:3|max:3|unique:transaction_currencies,code', + 'symbol' => 'required|min:1|max:8|unique:transaction_currencies,symbol', + 'decimal_places' => 'required|min:0|max:12|numeric', ]; if (intval($this->get('id')) > 0) { $rules = [ - 'code' => 'required|min:3|max:3', - 'name' => 'required|max:48|min:1', - 'symbol' => 'required|min:1|max:8', + 'name' => 'required|max:48|min:1', + 'code' => 'required|min:3|max:3', + 'symbol' => 'required|min:1|max:8', + 'decimal_places' => 'required|min:0|max:12|numeric', ]; } diff --git a/app/Http/Requests/JournalFormRequest.php b/app/Http/Requests/JournalFormRequest.php index df01440f4a..62dc5656d2 100644 --- a/app/Http/Requests/JournalFormRequest.php +++ b/app/Http/Requests/JournalFormRequest.php @@ -13,7 +13,6 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; -use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\TransactionType; @@ -43,30 +42,30 @@ class JournalFormRequest extends Request { $data = [ 'what' => $this->get('what'), // type. can be 'deposit', 'withdrawal' or 'transfer' - 'date' => new Carbon($this->get('date')), - 'tags' => explode(',', $this->getFieldOrEmptyString('tags')), - 'currency_id' => intval($this->get('amount_currency_id_amount')), + 'date' => $this->date('date'), + 'tags' => explode(',', $this->string('tags')), + 'currency_id' => $this->integer('amount_currency_id_amount'), // all custom fields: - 'interest_date' => $this->getDateOrNull('interest_date'), - 'book_date' => $this->getDateOrNull('book_date'), - 'process_date' => $this->getDateOrNull('process_date'), - 'due_date' => $this->getDateOrNull('due_date'), - 'payment_date' => $this->getDateOrNull('payment_date'), - 'invoice_date' => $this->getDateOrNull('invoice_date'), - 'internal_reference' => trim(strval($this->get('internal_reference'))), - 'notes' => trim(strval($this->get('notes'))), + 'interest_date' => $this->date('interest_date'), + 'book_date' => $this->date('book_date'), + 'process_date' => $this->date('process_date'), + 'due_date' => $this->date('due_date'), + 'payment_date' => $this->date('payment_date'), + 'invoice_date' => $this->date('invoice_date'), + 'internal_reference' => $this->string('internal_reference'), + 'notes' => $this->string('notes'), // transaction / journal data: - 'description' => $this->getFieldOrEmptyString('description'), - 'amount' => round($this->get('amount'), 2), - 'budget_id' => intval($this->get('budget_id')), - 'category' => $this->getFieldOrEmptyString('category'), - 'source_account_id' => intval($this->get('source_account_id')), - 'source_account_name' => $this->getFieldOrEmptyString('source_account_name'), - 'destination_account_id' => $this->getFieldOrEmptyString('destination_account_id'), - 'destination_account_name' => $this->getFieldOrEmptyString('destination_account_name'), - 'piggy_bank_id' => intval($this->get('piggy_bank_id')), + 'description' => $this->string('description'), + 'amount' => $this->float('amount'), + 'budget_id' => $this->integer('budget_id'), + 'category' => $this->string('category'), + 'source_account_id' => $this->integer('source_account_id'), + 'source_account_name' => $this->string('source_account_name'), + 'destination_account_id' => $this->string('destination_account_id'), + 'destination_account_name' => $this->string('destination_account_name'), + 'piggy_bank_id' => $this->integer('piggy_bank_id'), ]; @@ -94,7 +93,7 @@ class JournalFormRequest extends Request 'notes' => 'min:1,max:50000', // and then transaction rules: 'description' => 'required|between:1,255', - 'amount' => 'numeric|required|min:0.01', + 'amount' => 'numeric|required|more:0', 'budget_id' => 'mustExist:budgets,id|belongsToUser:budgets,id', 'category' => 'between:1,255', 'source_account_id' => 'numeric|belongsToUser:accounts,id', @@ -142,24 +141,4 @@ class JournalFormRequest extends Request return $rules; } - - /** - * @param string $field - * - * @return Carbon|null - */ - private function getDateOrNull(string $field) - { - return $this->get($field) ? new Carbon($this->get($field)) : null; - } - - /** - * @param string $field - * - * @return string - */ - private function getFieldOrEmptyString(string $field): string - { - return $this->get($field) ?? ''; - } } diff --git a/app/Http/Requests/PiggyBankFormRequest.php b/app/Http/Requests/PiggyBankFormRequest.php index 69c11d70b2..ba969dcf64 100644 --- a/app/Http/Requests/PiggyBankFormRequest.php +++ b/app/Http/Requests/PiggyBankFormRequest.php @@ -38,12 +38,12 @@ class PiggyBankFormRequest extends Request public function getPiggyBankData(): array { return [ - 'name' => trim($this->get('name')), + 'name' => $this->string('name'), 'startdate' => new Carbon, - 'account_id' => intval($this->get('account_id')), - 'targetamount' => round($this->get('targetamount'), 2), - 'targetdate' => strlen($this->get('targetdate')) > 0 ? new Carbon($this->get('targetdate')) : null, - 'note' => trim($this->get('note')), + 'account_id' => $this->integer('account_id'), + 'targetamount' => $this->float('targetamount'), + 'targetdate' => $this->date('targetdate'), + 'note' => $this->string('note'), ]; } @@ -63,8 +63,8 @@ class PiggyBankFormRequest extends Request $rules = [ 'name' => $nameRule, 'account_id' => 'required|belongsToUser:accounts', - 'targetamount' => 'required|min:0.01', - 'amount_currency_id_targetamount' => 'exists:transaction_currencies,id', + 'targetamount' => 'required|numeric|more:0', + 'amount_currency_id_targetamount' => 'required|exists:transaction_currencies,id', 'startdate' => 'date', 'targetdate' => $targetDateRule, 'order' => 'integer|min:1', diff --git a/app/Http/Requests/ReportFormRequest.php b/app/Http/Requests/ReportFormRequest.php new file mode 100644 index 0000000000..02a5f5c8e5 --- /dev/null +++ b/app/Http/Requests/ReportFormRequest.php @@ -0,0 +1,176 @@ +check(); + } + + /** + * @return Collection + */ + public function getAccountList(): Collection + { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $set = $this->get('accounts'); + $collection = new Collection; + if (is_array($set)) { + foreach ($set as $accountId) { + $account = $repository->find(intval($accountId)); + if (!is_null($account->id)) { + $collection->push($account); + } + } + } + + return $collection; + } + + /** + * @return Collection + */ + public function getBudgetList(): Collection + { + /** @var BudgetRepositoryInterface $repository */ + $repository = app(BudgetRepositoryInterface::class); + $set = $this->get('budget'); + $collection = new Collection; + if (is_array($set)) { + foreach ($set as $budgetId) { + $budget = $repository->find(intval($budgetId)); + if (!is_null($budget->id)) { + $collection->push($budget); + } + } + } + + return $collection; + } + + /** + * @return Collection + */ + public function getCategoryList(): Collection + { + /** @var CategoryRepositoryInterface $repository */ + $repository = app(CategoryRepositoryInterface::class); + $set = $this->get('category'); + $collection = new Collection; + if (is_array($set)) { + foreach ($set as $categoryId) { + $category = $repository->find(intval($categoryId)); + if (!is_null($category->id)) { + $collection->push($category); + } + } + } + + return $collection; + } + + /** + * @return Carbon + * @throws FireflyException + */ + public function getEndDate(): Carbon + { + $date = new Carbon; + $range = $this->get('daterange'); + $parts = explode(' - ', strval($range)); + if (count($parts) === 2) { + try { + $date = new Carbon($parts[1]); + } catch (Exception $e) { + throw new FireflyException(sprintf('"%s" is not a valid date range.', $range)); + } + } + + return $date; + } + + /** + * @return Carbon + * @throws FireflyException + */ + public function getStartDate(): Carbon + { + $date = new Carbon; + $range = $this->get('daterange'); + $parts = explode(' - ', strval($range)); + if (count($parts) === 2) { + try { + $date = new Carbon($parts[0]); + } catch (Exception $e) { + throw new FireflyException(sprintf('"%s" is not a valid date range.', $range)); + } + } + + return $date; + } + + /** + * @return Collection + */ + public function getTagList(): Collection + { + /** @var TagRepositoryInterface $repository */ + $repository = app(TagRepositoryInterface::class); + $set = $this->get('tag'); + $collection = new Collection; + if (is_array($set)) { + foreach ($set as $tagTag) { + $tag = $repository->findByTag($tagTag); + if (!is_null($tag->id)) { + $collection->push($tag); + } + } + } + + return $collection; + } + + /** + * @return array + */ + public function rules(): array + { + return [ + 'report_type' => 'in:audit,default,category,budget,tag', + ]; + } + +} diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index 84cb318d3e..e73b17ac60 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -13,6 +13,7 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; +use Carbon\Carbon; use Illuminate\Foundation\Http\FormRequest; /** @@ -20,7 +21,123 @@ use Illuminate\Foundation\Http\FormRequest; * * @package FireflyIII\Http\Requests */ -abstract class Request extends FormRequest +class Request extends FormRequest { - // + /** + * @param string $field + * + * @return bool + */ + protected function boolean(string $field): bool + { + return intval($this->input($field)) === 1; + } + + /** + * @param string $field + * + * @return Carbon|null + */ + protected function date(string $field) + { + return $this->get($field) ? new Carbon($this->get($field)) : null; + } + + /** + * @param string $field + * + * @return float + */ + protected function float(string $field): float + { + return round($this->input($field), 12); + } + + /** + * @param string $field + * @param string $type + * + * @return array + */ + protected function getArray(string $field, string $type): array + { + $original = $this->get($field); + $return = []; + foreach ($original as $index => $value) { + $return[$index] = $this->$type($value); + } + + return $return; + } + + /** + * @param string $field + * + * @return int + */ + protected function integer(string $field): int + { + return intval($this->get($field)); + } + + /** + * @param string $field + * + * @return string + */ + protected function string(string $field): string + { + $string = $this->get($field) ?? ''; + $search = [ + "\u{0001}", // start of heading + "\u{0002}", // start of text + "\u{0003}", // end of text + "\u{0004}", // end of transmission + "\u{0005}", // enquiry + "\u{0006}", // ACK + "\u{0007}", // BEL + "\u{0008}", // backspace + "\u{000E}", // shift out + "\u{000F}", // shift in + "\u{0010}", // data link escape + "\u{0011}", // DC1 + "\u{0012}", // DC2 + "\u{0013}", // DC3 + "\u{0014}", // DC4 + "\u{0015}", // NAK + "\u{0016}", // SYN + "\u{0017}", // ETB + "\u{0018}", // CAN + "\u{0019}", // EM + "\u{001A}", // SUB + "\u{001B}", // escape + "\u{001C}", // file separator + "\u{001D}", // group separator + "\u{001E}", // record separator + "\u{001F}", // unit separator + "\u{007F}", // DEL + "\u{00A0}", // non-breaking space + "\u{1680}", // ogham space mark + "\u{180E}", // mongolian vowel separator + "\u{2000}", // en quad + "\u{2001}", // em quad + "\u{2002}", // en space + "\u{2003}", // em space + "\u{2004}", // three-per-em space + "\u{2005}", // four-per-em space + "\u{2006}", // six-per-em space + "\u{2007}", // figure space + "\u{2008}", // punctuation space + "\u{2009}", // thin space + "\u{200A}", // hair space + "\u{200B}", // zero width space + "\u{202F}", // narrow no-break space + "\u{3000}", // ideographic space + "\u{FEFF}", // zero width no -break space + ]; + $replace = "\x20"; // plain old normal space + $string = str_replace($search, $replace, $string); + + return trim($string); + } } diff --git a/app/Http/Requests/RuleFormRequest.php b/app/Http/Requests/RuleFormRequest.php index 814f0c560d..6295bd9e3f 100644 --- a/app/Http/Requests/RuleFormRequest.php +++ b/app/Http/Requests/RuleFormRequest.php @@ -38,17 +38,17 @@ class RuleFormRequest extends Request public function getRuleData(): array { return [ - 'title' => trim($this->get('title')), - 'active' => intval($this->get('active')) == 1, - 'trigger' => trim($this->get('trigger')), - 'description' => trim($this->get('description')), + 'title' => $this->string('title'), + 'active' => $this->boolean('active'), + 'trigger' => $this->string('trigger'), + 'description' => $this->string('description'), 'rule-triggers' => $this->get('rule-trigger'), 'rule-trigger-values' => $this->get('rule-trigger-value'), 'rule-trigger-stop' => $this->get('rule-trigger-stop'), 'rule-actions' => $this->get('rule-action'), 'rule-action-values' => $this->get('rule-action-value'), 'rule-action-stop' => $this->get('rule-action-stop'), - 'stop_processing' => intval($this->get('stop_processing')) === 1, + 'stop_processing' => $this->boolean('stop_processing'), ]; } diff --git a/app/Http/Requests/RuleGroupFormRequest.php b/app/Http/Requests/RuleGroupFormRequest.php index 9fa09fdefa..7d2250882f 100644 --- a/app/Http/Requests/RuleGroupFormRequest.php +++ b/app/Http/Requests/RuleGroupFormRequest.php @@ -10,13 +10,6 @@ */ declare(strict_types = 1); -/** - * RuleGroupFormRequest.php - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ namespace FireflyIII\Http\Requests; @@ -45,8 +38,8 @@ class RuleGroupFormRequest extends Request public function getRuleGroupData(): array { return [ - 'title' => trim($this->input('title')), - 'description' => trim($this->input('description')), + 'title' => $this->string('title'), + 'description' => $this->string('description'), ]; } @@ -56,7 +49,7 @@ class RuleGroupFormRequest extends Request public function rules() { /** @var RuleGroupRepositoryInterface $repository */ - $repository = app(RuleGroupRepositoryInterface::class, [auth()->user()]); + $repository = app(RuleGroupRepositoryInterface::class); $titleRule = 'required|between:1,100|uniqueObjectForUser:rule_groups,title'; if (!is_null($repository->find(intval($this->get('id')))->id)) { $titleRule = 'required|between:1,100|uniqueObjectForUser:rule_groups,title,' . intval($this->get('id')); diff --git a/app/Http/Requests/SplitJournalFormRequest.php b/app/Http/Requests/SplitJournalFormRequest.php index ea246fc2d1..3d82027c6b 100644 --- a/app/Http/Requests/SplitJournalFormRequest.php +++ b/app/Http/Requests/SplitJournalFormRequest.php @@ -13,8 +13,7 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; -use Carbon\Carbon; - +use Steam; /** * Class SplitJournalFormRequest @@ -38,18 +37,18 @@ class SplitJournalFormRequest extends Request public function getSplitData(): array { $data = [ - 'id' => $this->get('id') ?? 0, - 'journal_description' => $this->get('journal_description'), - 'journal_currency_id' => intval($this->get('journal_currency_id')), - 'journal_source_account_id' => intval($this->get('journal_source_account_id')), - 'journal_source_account_name' => $this->get('journal_source_account_name'), - 'journal_destination_account_id' => intval($this->get('journal_destination_account_id')), - 'journal_destination_account_name' => $this->get('journal_source_destination_name'), - 'date' => new Carbon($this->get('date')), - 'what' => $this->get('what'), - 'interest_date' => $this->get('interest_date') ? new Carbon($this->get('interest_date')) : null, - 'book_date' => $this->get('book_date') ? new Carbon($this->get('book_date')) : null, - 'process_date' => $this->get('process_date') ? new Carbon($this->get('process_date')) : null, + 'id' => $this->integer('id'), + 'journal_description' => $this->string('journal_description'), + 'journal_currency_id' => $this->integer('journal_currency_id'), + 'journal_source_account_id' => $this->integer('journal_source_account_id'), + 'journal_source_account_name' => $this->string('journal_source_account_name'), + 'journal_destination_account_id' => $this->integer('journal_destination_account_id'), + 'journal_destination_account_name' => $this->string('journal_source_destination_name'), + 'date' => $this->date('date'), + 'what' => $this->string('what'), + 'interest_date' => $this->date('interest_date'), + 'book_date' => $this->date('book_date'), + 'process_date' => $this->date('process_date'), 'transactions' => $this->getTransactionData(), ]; @@ -87,27 +86,30 @@ class SplitJournalFormRequest extends Request */ private function getTransactionData(): array { + $descriptions = $this->getArray('description', 'string'); + $categories = $this->getArray('category', 'string'); + $amounts = $this->getArray('amount', 'float'); + $budgets = $this->getArray('amount', 'integer'); + $srcAccountIds = $this->getArray('source_account_id', 'integer'); + $srcAccountNames = $this->getArray('source_account_name', 'string'); + $dstAccountIds = $this->getArray('destination_account_id', 'integer'); + $dstAccountNames = $this->getArray('destination_account_name', 'string'); + $piggyBankIds = $this->getArray('piggy_bank_id', 'integer'); + $return = []; // description is leading because it is one of the mandatory fields. - foreach ($this->get('description') as $index => $description) { + foreach ($descriptions as $index => $description) { + $category = $categories[$index] ?? ''; $transaction = [ 'description' => $description, - 'amount' => round($this->get('amount')[$index], 2), - 'budget_id' => $this->get('budget_id')[$index] ? intval($this->get('budget_id')[$index]) : 0, - 'category' => $this->get('category')[$index] ?? '', - 'source_account_id' => isset($this->get('source_account_id')[$index]) - ? intval($this->get('source_account_id')[$index]) - : intval( - $this->get('journal_source_account_id') - ), - 'source_account_name' => $this->get('source_account_name')[$index] ?? '', - 'piggy_bank_id' => isset($this->get('piggy_bank_id')[$index]) ? intval($this->get('piggy_bank_id')[$index]) : 0, - 'destination_account_id' => isset($this->get('destination_account_id')[$index]) - ? intval($this->get('destination_account_id')[$index]) - : intval( - $this->get('journal_destination_account_id') - ), - 'destination_account_name' => $this->get('destination_account_name')[$index] ?? '', + 'amount' => Steam::positive($amounts[$index]), + 'budget_id' => $budgets[$index] ?? 0, + 'category' => $category, + 'source_account_id' => $srcAccountIds[$index] ?? $this->get('journal_source_account_id'), + 'source_account_name' => $srcAccountNames[$index] ?? '', + 'piggy_bank_id' => $piggyBankIds[$index] ?? 0, + 'destination_account_id' => $dstAccountIds[$index] ?? $this->get('journal_destination_account_id'), + 'destination_account_name' => $dstAccountNames[$index] ?? '', ]; $return[] = $transaction; } diff --git a/app/Http/Requests/TagFormRequest.php b/app/Http/Requests/TagFormRequest.php index c0501c1798..544b53c0b0 100644 --- a/app/Http/Requests/TagFormRequest.php +++ b/app/Http/Requests/TagFormRequest.php @@ -35,12 +35,12 @@ class TagFormRequest extends Request /** * @return array */ - public function collectTagData() :array + public function collectTagData(): array { if ($this->get('setTag') == 'true') { - $latitude = $this->get('latitude'); - $longitude = $this->get('longitude'); - $zoomLevel = $this->get('zoomLevel'); + $latitude = $this->string('latitude'); + $longitude = $this->string('longitude'); + $zoomLevel = $this->integer('zoomLevel'); } else { $latitude = null; $longitude = null; @@ -49,13 +49,13 @@ class TagFormRequest extends Request $date = $this->get('date') ?? ''; $data = [ - 'tag' => $this->get('tag'), - 'date' => strlen($date) > 0 ? new Carbon($date) : null, - 'description' => $this->get('description') ?? '', + 'tag' => $this->string('tag'), + 'date' => $this->date($date), + 'description' => $this->string('description'), 'latitude' => $latitude, 'longitude' => $longitude, 'zoomLevel' => $zoomLevel, - 'tagMode' => $this->get('tagMode'), + 'tagMode' => $this->string('tagMode'), ]; return $data; diff --git a/app/Http/Requests/UserFormRequest.php b/app/Http/Requests/UserFormRequest.php new file mode 100644 index 0000000000..436e3c9d33 --- /dev/null +++ b/app/Http/Requests/UserFormRequest.php @@ -0,0 +1,59 @@ +check(); + } + + /** + * @return array + */ + public function getUserData(): array + { + return [ + 'email' => $this->string('email'), + 'blocked' => $this->integer('blocked'), + 'blocked_code' => $this->string('blocked_code'), + 'password' => $this->string('password'), + ]; + } + + /** + * @return array + */ + public function rules() + { + return [ + 'id' => 'required|exists:users,id', + 'email' => 'required', + 'password' => 'confirmed', + 'blocked_code' => 'between:0,30', + 'blocked' => 'between:0,1|numeric', + ]; + } +} diff --git a/app/Http/breadcrumbs.php b/app/Http/breadcrumbs.php index 44c4fc707e..05bf0acb56 100644 --- a/app/Http/breadcrumbs.php +++ b/app/Http/breadcrumbs.php @@ -17,15 +17,18 @@ use FireflyIII\Models\Account; use FireflyIII\Models\Attachment; use FireflyIII\Models\Bill; use FireflyIII\Models\Budget; +use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\Category; -use FireflyIII\Models\LimitRepetition; +use FireflyIII\Models\ImportJob; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\Rule; use FireflyIII\Models\RuleGroup; use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; use FireflyIII\User; +use Illuminate\Support\Collection; /** * HOME @@ -66,22 +69,37 @@ Breadcrumbs::register( Breadcrumbs::register( 'accounts.show', function (BreadCrumbGenerator $breadcrumbs, Account $account) { $what = config('firefly.shortNamesByFullName.' . $account->accountType->type); + $breadcrumbs->parent('accounts.index', $what); - $breadcrumbs->push(e($account->name), route('accounts.show', [$account->id])); + $breadcrumbs->push($account->name, route('accounts.show', [$account->id])); } ); Breadcrumbs::register( - 'accounts.show.date', function (BreadCrumbGenerator $breadcrumbs, Account $account, Carbon $date) { + 'accounts.show.date', function (BreadCrumbGenerator $breadcrumbs, Account $account, Carbon $start, Carbon $end) { + + $startString = $start->formatLocalized(strval(trans('config.month_and_day'))); + $endString = $end->formatLocalized(strval(trans('config.month_and_day'))); + $title = sprintf('%s (%s)', $account->name, trans('firefly.from_to', ['start' => $startString, 'end' => $endString])); + $breadcrumbs->parent('accounts.show', $account); - - $range = Preferences::get('viewRange', '1M')->data; - $title = $account->name . ' (' . Navigation::periodShow($date, $range) . ')'; - - $breadcrumbs->push($title, route('accounts.show.date', [$account->id, $date->format('Y-m-d')])); + $breadcrumbs->push($title, route('accounts.show.date', [$account->id, $start->format('Y-m-d')])); } ); +Breadcrumbs::register( + 'accounts.show.all', function (BreadCrumbGenerator $breadcrumbs, Account $account, Carbon $start, Carbon $end) { + + $startString = $start->formatLocalized(strval(trans('config.month_and_day'))); + $endString = $end->formatLocalized(strval(trans('config.month_and_day'))); + $title = sprintf('%s (%s)', $account->name, trans('firefly.from_to', ['start' => $startString, 'end' => $endString])); + + $breadcrumbs->parent('accounts.show', $account); + $breadcrumbs->push($title, route('accounts.show.all', [$account->id, $start->format('Y-m-d')])); +} +); + + Breadcrumbs::register( 'accounts.delete', function (BreadCrumbGenerator $breadcrumbs, Account $account) { $breadcrumbs->parent('accounts.show', $account); @@ -122,6 +140,12 @@ Breadcrumbs::register( $breadcrumbs->push(trans('firefly.single_user_administration', ['email' => $user->email]), route('admin.users.show', [$user->id])); } ); +Breadcrumbs::register( + 'admin.users.edit', function (BreadCrumbGenerator $breadcrumbs, User $user) { + $breadcrumbs->parent('admin.users'); + $breadcrumbs->push(trans('firefly.edit_user', ['email' => $user->email]), route('admin.users.edit', [$user->id])); +} +); Breadcrumbs::register( 'admin.users.domains', function (BreadCrumbGenerator $breadcrumbs) { @@ -236,21 +260,35 @@ Breadcrumbs::register( ); Breadcrumbs::register( - 'budgets.noBudget', function (BreadCrumbGenerator $breadcrumbs, $subTitle) { + 'budgets.no-budget', function (BreadCrumbGenerator $breadcrumbs, $subTitle) { $breadcrumbs->parent('budgets.index'); - $breadcrumbs->push($subTitle, route('budgets.noBudget')); + $breadcrumbs->push($subTitle, route('budgets.no-budget')); } ); Breadcrumbs::register( - 'budgets.show', function (BreadCrumbGenerator $breadcrumbs, Budget $budget, LimitRepetition $repetition = null) { + 'budgets.show', function (BreadCrumbGenerator $breadcrumbs, Budget $budget) { $breadcrumbs->parent('budgets.index'); $breadcrumbs->push(e($budget->name), route('budgets.show', [$budget->id])); - if (!is_null($repetition) && !is_null($repetition->id)) { - $breadcrumbs->push( - Navigation::periodShow($repetition->startdate, $repetition->budgetLimit->repeat_freq), route('budgets.show', [$budget->id, $repetition->id]) - ); - } +} +); + +Breadcrumbs::register( + 'budgets.show.limit', function (BreadCrumbGenerator $breadcrumbs, Budget $budget, BudgetLimit $budgetLimit) { + $breadcrumbs->parent('budgets.index'); + $breadcrumbs->push(e($budget->name), route('budgets.show', [$budget->id])); + + $title = trans( + 'firefly.budget_in_period_breadcrumb', [ + 'name' => $budget->name, + 'start' => $budgetLimit->start_date->formatLocalized(strval(trans('config.month_and_day'))), + 'end' => $budgetLimit->end_date->formatLocalized(strval(trans('config.month_and_day'))), + ] + ); + + $breadcrumbs->push( + $title, route('budgets.show.limit', [$budget->id, $budgetLimit->id]) + ); } ); @@ -291,6 +329,14 @@ Breadcrumbs::register( } ); +Breadcrumbs::register( + 'categories.show.all', function (BreadCrumbGenerator $breadcrumbs, Category $category) { + $breadcrumbs->parent('categories.index'); + $breadcrumbs->push(e($category->name) . '(' . strtolower(trans('firefly.all_periods')) . ')', route('categories.show.all', [$category->id])); + +} +); + Breadcrumbs::register( 'categories.show.date', function (BreadCrumbGenerator $breadcrumbs, Category $category, Carbon $date) { @@ -305,9 +351,9 @@ Breadcrumbs::register( ); Breadcrumbs::register( - 'categories.noCategory', function (BreadCrumbGenerator $breadcrumbs, $subTitle) { + 'categories.no-category', function (BreadCrumbGenerator $breadcrumbs, $subTitle) { $breadcrumbs->parent('categories.index'); - $breadcrumbs->push($subTitle, route('categories.noCategory')); + $breadcrumbs->push($subTitle, route('categories.no-category')); } ); @@ -315,29 +361,29 @@ Breadcrumbs::register( * CURRENCIES */ Breadcrumbs::register( - 'currency.index', function (BreadCrumbGenerator $breadcrumbs) { + 'currencies.index', function (BreadCrumbGenerator $breadcrumbs) { $breadcrumbs->parent('home'); - $breadcrumbs->push(trans('firefly.currencies'), route('currency.index')); + $breadcrumbs->push(trans('firefly.currencies'), route('currencies.index')); } ); Breadcrumbs::register( - 'currency.create', function (BreadCrumbGenerator $breadcrumbs) { - $breadcrumbs->parent('currency.index'); - $breadcrumbs->push(trans('firefly.create_currency'), route('currency.create')); + 'currencies.create', function (BreadCrumbGenerator $breadcrumbs) { + $breadcrumbs->parent('currencies.index'); + $breadcrumbs->push(trans('firefly.create_currency'), route('currencies.create')); } ); Breadcrumbs::register( - 'currency.edit', function (BreadCrumbGenerator $breadcrumbs, TransactionCurrency $currency) { - $breadcrumbs->parent('currency.index'); - $breadcrumbs->push(trans('breadcrumbs.edit_currency', ['name' => e($currency->name)]), route('currency.edit', [$currency->id])); + 'currencies.edit', function (BreadCrumbGenerator $breadcrumbs, TransactionCurrency $currency) { + $breadcrumbs->parent('currencies.index'); + $breadcrumbs->push(trans('breadcrumbs.edit_currency', ['name' => e($currency->name)]), route('currencies.edit', [$currency->id])); } ); Breadcrumbs::register( - 'currency.delete', function (BreadCrumbGenerator $breadcrumbs, TransactionCurrency $currency) { - $breadcrumbs->parent('currency.index'); - $breadcrumbs->push(trans('breadcrumbs.delete_currency', ['name' => e($currency->name)]), route('currency.delete', [$currency->id])); + 'currencies.delete', function (BreadCrumbGenerator $breadcrumbs, TransactionCurrency $currency) { + $breadcrumbs->parent('currencies.index'); + $breadcrumbs->push(trans('breadcrumbs.delete_currency', ['name' => e($currency->name)]), route('currencies.delete', [$currency->id])); } ); @@ -384,25 +430,68 @@ Breadcrumbs::register( 'piggy-banks.show', function (BreadCrumbGenerator $breadcrumbs, PiggyBank $piggyBank) { $breadcrumbs->parent('piggy-banks.index'); $breadcrumbs->push(e($piggyBank->name), route('piggy-banks.show', [$piggyBank->id])); - } ); +Breadcrumbs::register( + 'piggy-banks.add-money-mobile', function (BreadCrumbGenerator $breadcrumbs, PiggyBank $piggyBank) { + $breadcrumbs->parent('piggy-banks.show', $piggyBank); + $breadcrumbs->push(trans('firefly.add_money_to_piggy', ['name' => $piggyBank->name]), route('piggy-banks.add-money-mobile', [$piggyBank->id])); +} +); + +Breadcrumbs::register( + 'piggy-banks.remove-money-mobile', function (BreadCrumbGenerator $breadcrumbs, PiggyBank $piggyBank) { + $breadcrumbs->parent('piggy-banks.show', $piggyBank); + $breadcrumbs->push( + trans('firefly.remove_money_from_piggy_title', ['name' => $piggyBank->name]), route('piggy-banks.remove-money-mobile', [$piggyBank->id]) + ); +} +); + +/** + * IMPORT + */ +Breadcrumbs::register( + 'import.index', function (BreadCrumbGenerator $breadcrumbs) { + $breadcrumbs->parent('home'); + $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::register( + 'import.finished', 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])); +} +); + + /** * PREFERENCES */ Breadcrumbs::register( - 'preferences', function (BreadCrumbGenerator $breadcrumbs) { + 'preferences.index', function (BreadCrumbGenerator $breadcrumbs) { $breadcrumbs->parent('home'); - $breadcrumbs->push(trans('breadcrumbs.preferences'), route('preferences')); - + $breadcrumbs->push(trans('breadcrumbs.preferences'), route('preferences.index')); } ); Breadcrumbs::register( 'preferences.code', function (BreadCrumbGenerator $breadcrumbs) { $breadcrumbs->parent('home'); - $breadcrumbs->push(trans('breadcrumbs.preferences'), route('preferences')); + $breadcrumbs->push(trans('breadcrumbs.preferences'), route('preferences.index')); } ); @@ -411,22 +500,22 @@ Breadcrumbs::register( * PROFILE */ Breadcrumbs::register( - 'profile', function (BreadCrumbGenerator $breadcrumbs) { + 'profile.index', function (BreadCrumbGenerator $breadcrumbs) { $breadcrumbs->parent('home'); - $breadcrumbs->push(trans('breadcrumbs.profile'), route('profile')); + $breadcrumbs->push(trans('breadcrumbs.profile'), route('profile.index')); } ); Breadcrumbs::register( 'profile.change-password', function (BreadCrumbGenerator $breadcrumbs) { - $breadcrumbs->parent('profile'); + $breadcrumbs->parent('profile.index'); $breadcrumbs->push(trans('breadcrumbs.changePassword'), route('profile.change-password')); } ); Breadcrumbs::register( 'profile.delete-account', function (BreadCrumbGenerator $breadcrumbs) { - $breadcrumbs->parent('profile'); + $breadcrumbs->parent('profile.index'); $breadcrumbs->push(trans('firefly.delete_account'), route('profile.delete-account')); } @@ -443,16 +532,63 @@ Breadcrumbs::register( ); Breadcrumbs::register( - 'reports.report', function (BreadCrumbGenerator $breadcrumbs, Carbon $start, Carbon $end, $reportType, $accountIds) { + 'reports.report.audit', function (BreadCrumbGenerator $breadcrumbs, string $accountIds, Carbon $start, Carbon $end) { $breadcrumbs->parent('reports.index'); $monthFormat = (string)trans('config.month_and_day'); - $title = (string)trans( - 'firefly.report_' . $reportType, - ['start' => $start->formatLocalized($monthFormat), 'end' => $end->formatLocalized($monthFormat)] - ); + $startString = $start->formatLocalized($monthFormat); + $endString = $end->formatLocalized($monthFormat); + $title = (string)trans('firefly.report_audit', ['start' => $startString, 'end' => $endString]); - $breadcrumbs->push($title, route('reports.report', [$reportType, $start->format('Ymd'), $end->format('Ymd'), $accountIds])); + $breadcrumbs->push($title, route('reports.report.audit', [$accountIds, $start->format('Ymd'), $end->format('Ymd')])); +} +); +Breadcrumbs::register( + 'reports.report.budget', function (BreadCrumbGenerator $breadcrumbs, string $accountIds, string $budgetIds, Carbon $start, Carbon $end) { + $breadcrumbs->parent('reports.index'); + + $monthFormat = (string)trans('config.month_and_day'); + $startString = $start->formatLocalized($monthFormat); + $endString = $end->formatLocalized($monthFormat); + $title = (string)trans('firefly.report_budget', ['start' => $startString, 'end' => $endString]); + + $breadcrumbs->push($title, route('reports.report.budget', [$accountIds, $budgetIds, $start->format('Ymd'), $end->format('Ymd')])); +} +); + +Breadcrumbs::register( + 'reports.report.category', function (BreadCrumbGenerator $breadcrumbs, string $accountIds, string $categoryIds, Carbon $start, Carbon $end) { + $breadcrumbs->parent('reports.index'); + + $monthFormat = (string)trans('config.month_and_day'); + $startString = $start->formatLocalized($monthFormat); + $endString = $end->formatLocalized($monthFormat); + $title = (string)trans('firefly.report_category', ['start' => $startString, 'end' => $endString]); + + $breadcrumbs->push($title, route('reports.report.category', [$accountIds, $categoryIds, $start->format('Ymd'), $end->format('Ymd')])); +} +); + +Breadcrumbs::register( + 'reports.report.default', function (BreadCrumbGenerator $breadcrumbs, string $accountIds, Carbon $start, Carbon $end) { + $breadcrumbs->parent('reports.index'); + + $monthFormat = (string)trans('config.month_and_day'); + $startString = $start->formatLocalized($monthFormat); + $endString = $end->formatLocalized($monthFormat); + $title = (string)trans('firefly.report_default', ['start' => $startString, 'end' => $endString]); + + $breadcrumbs->push($title, route('reports.report.default', [$accountIds, $start->format('Ymd'), $end->format('Ymd')])); +} +); + +/** + * New user Controller + */ +Breadcrumbs::register( + 'new-user.index', function (BreadCrumbGenerator $breadcrumbs) { + $breadcrumbs->parent('home'); + $breadcrumbs->push(trans('firefly.getting_started'), route('new-user.index')); } ); @@ -467,47 +603,54 @@ Breadcrumbs::register( ); Breadcrumbs::register( - 'rules.rule.create', function (BreadCrumbGenerator $breadcrumbs, RuleGroup $ruleGroup) { + 'rules.create', function (BreadCrumbGenerator $breadcrumbs, RuleGroup $ruleGroup) { $breadcrumbs->parent('rules.index'); - $breadcrumbs->push(trans('firefly.make_new_rule', ['title' => $ruleGroup->title]), route('rules.rule.create', [$ruleGroup])); + $breadcrumbs->push(trans('firefly.make_new_rule', ['title' => $ruleGroup->title]), route('rules.create', [$ruleGroup])); } ); Breadcrumbs::register( - 'rules.rule.edit', function (BreadCrumbGenerator $breadcrumbs, Rule $rule) { + 'rules.edit', function (BreadCrumbGenerator $breadcrumbs, Rule $rule) { $breadcrumbs->parent('rules.index'); - $breadcrumbs->push(trans('firefly.edit_rule', ['title' => $rule->title]), route('rules.rule.edit', [$rule])); + $breadcrumbs->push(trans('firefly.edit_rule', ['title' => $rule->title]), route('rules.edit', [$rule])); } ); Breadcrumbs::register( - 'rules.rule.delete', function (BreadCrumbGenerator $breadcrumbs, Rule $rule) { + 'rules.delete', function (BreadCrumbGenerator $breadcrumbs, Rule $rule) { $breadcrumbs->parent('rules.index'); - $breadcrumbs->push(trans('firefly.delete_rule', ['title' => $rule->title]), route('rules.rule.delete', [$rule])); + $breadcrumbs->push(trans('firefly.delete_rule', ['title' => $rule->title]), route('rules.delete', [$rule])); } ); Breadcrumbs::register( - 'rules.rule-group.create', function (BreadCrumbGenerator $breadcrumbs) { + 'rule-groups.create', function (BreadCrumbGenerator $breadcrumbs) { $breadcrumbs->parent('rules.index'); - $breadcrumbs->push(trans('firefly.make_new_rule_group'), route('rules.rule-group.create')); + $breadcrumbs->push(trans('firefly.make_new_rule_group'), route('rule-groups.create')); } ); Breadcrumbs::register( - 'rules.rule-group.edit', function (BreadCrumbGenerator $breadcrumbs, RuleGroup $ruleGroup) { + 'rule-groups.edit', function (BreadCrumbGenerator $breadcrumbs, RuleGroup $ruleGroup) { $breadcrumbs->parent('rules.index'); - $breadcrumbs->push(trans('firefly.edit_rule_group', ['title' => $ruleGroup->title]), route('rules.rule-group.edit', [$ruleGroup])); + $breadcrumbs->push(trans('firefly.edit_rule_group', ['title' => $ruleGroup->title]), route('rule-groups.edit', [$ruleGroup])); } ); Breadcrumbs::register( - 'rules.rule-group.delete', function (BreadCrumbGenerator $breadcrumbs, RuleGroup $ruleGroup) { + 'rule-groups.delete', function (BreadCrumbGenerator $breadcrumbs, RuleGroup $ruleGroup) { $breadcrumbs->parent('rules.index'); - $breadcrumbs->push(trans('firefly.delete_rule_group', ['title' => $ruleGroup->title]), route('rules.rule-group.delete', [$ruleGroup])); + $breadcrumbs->push(trans('firefly.delete_rule_group', ['title' => $ruleGroup->title]), route('rule-groups.delete', [$ruleGroup])); } ); Breadcrumbs::register( - 'rules.rule-group.select_transactions', function (BreadCrumbGenerator $breadcrumbs, RuleGroup $ruleGroup) { + 'rule-groups.select-transactions', function (BreadCrumbGenerator $breadcrumbs, RuleGroup $ruleGroup) { + $breadcrumbs->parent('rules.index'); + $breadcrumbs->push(trans('firefly.rule_group_select_transactions', ['title' => $ruleGroup->title]), route('rule-groups.select-transactions', [$ruleGroup])); +} +); + +Breadcrumbs::register( + 'rule-groups.select_transactions', function (BreadCrumbGenerator $breadcrumbs, RuleGroup $ruleGroup) { $breadcrumbs->parent('rules.index'); $breadcrumbs->push( - trans('firefly.execute_group_on_existing_transactions', ['title' => $ruleGroup->title]), route('rules.rule-group.select_transactions', [$ruleGroup]) + trans('firefly.execute_group_on_existing_transactions', ['title' => $ruleGroup->title]), route('rule-groups.select_transactions', [$ruleGroup]) ); } ); @@ -517,9 +660,9 @@ Breadcrumbs::register( * SEARCH */ Breadcrumbs::register( - 'search', function (BreadCrumbGenerator $breadcrumbs, $query) { + 'search.index', function (BreadCrumbGenerator $breadcrumbs, $query) { $breadcrumbs->parent('home'); - $breadcrumbs->push(trans('breadcrumbs.searchResult', ['query' => e($query)]), route('search')); + $breadcrumbs->push(trans('breadcrumbs.searchResult', ['query' => e($query)]), route('search.index')); } ); @@ -572,6 +715,28 @@ Breadcrumbs::register( $breadcrumbs->push(trans('breadcrumbs.' . $what . '_list'), route('transactions.index', [$what])); } ); + +Breadcrumbs::register( + 'transactions.index.all', function (BreadCrumbGenerator $breadcrumbs, string $what) { + $breadcrumbs->parent('transactions.index', $what); + + $title = sprintf('%s (%s)', trans('breadcrumbs.' . $what . '_list'), strtolower(trans('firefly.everything'))); + + $breadcrumbs->push($title, route('transactions.index.all', [$what])); +} +); + +Breadcrumbs::register( + 'transactions.index.date', function (BreadCrumbGenerator $breadcrumbs, string $what, Carbon $date) { + $breadcrumbs->parent('transactions.index', $what); + + $range = Preferences::get('viewRange', '1M')->data; + $title = trans('breadcrumbs.' . $what . '_list') . ' (' . Navigation::periodShow($date, $range) . ')'; + + $breadcrumbs->push($title, route('transactions.index.date', [$what, $date->format('Y-m-d')])); +} +); + Breadcrumbs::register( 'transactions.create', function (BreadCrumbGenerator $breadcrumbs, string $what) { $breadcrumbs->parent('transactions.index', $what); @@ -595,26 +760,53 @@ Breadcrumbs::register( Breadcrumbs::register( 'transactions.show', function (BreadCrumbGenerator $breadcrumbs, TransactionJournal $journal) { - $what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); + $what = strtolower($journal->transactionType->type); $breadcrumbs->parent('transactions.index', $what); $breadcrumbs->push($journal->description, route('transactions.show', [$journal->id])); - } ); +Breadcrumbs::register( + 'transactions.convert', function (BreadCrumbGenerator $breadcrumbs, TransactionType $destinationType, TransactionJournal $journal) { + + $breadcrumbs->parent('transactions.show', $journal); + $breadcrumbs->push( + trans('firefly.convert_to_' . $destinationType->type, ['description' => $journal->description]), + route('transactions.convert.index', [strtolower($destinationType->type), $journal->id]) + ); +} +); + +/** + * MASS TRANSACTION EDIT / DELETE + */ +Breadcrumbs::register( + 'transactions.mass.edit', function (BreadCrumbGenerator $breadcrumbs, Collection $journals) { + + $journalIds = $journals->pluck('id')->toArray(); + $what = strtolower($journals->first()->transactionType->type); + $breadcrumbs->parent('transactions.index', $what); + $breadcrumbs->push(trans('firefly.mass_edit_journals'), route('transactions.mass.edit', $journalIds)); +} +); + +Breadcrumbs::register( + 'transactions.mass.delete', function (BreadCrumbGenerator $breadcrumbs, Collection $journals) { + + $journalIds = $journals->pluck('id')->toArray(); + $what = strtolower($journals->first()->transactionType->type); + $breadcrumbs->parent('transactions.index', $what); + $breadcrumbs->push(trans('firefly.mass_edit_journals'), route('transactions.mass.delete', $journalIds)); +} +); + + /** * SPLIT */ Breadcrumbs::register( - 'transactions.edit-split', function (BreadCrumbGenerator $breadcrumbs, TransactionJournal $journal) { + 'transactions.split.edit', function (BreadCrumbGenerator $breadcrumbs, TransactionJournal $journal) { $breadcrumbs->parent('transactions.show', $journal); - $breadcrumbs->push(trans('breadcrumbs.edit_journal', ['description' => $journal->description]), route('transactions.edit-split', [$journal->id])); -} -); - -Breadcrumbs::register( - 'split.journal.create', function (BreadCrumbGenerator $breadcrumbs, string $what) { - $breadcrumbs->parent('transactions.index', $what); - $breadcrumbs->push(trans('breadcrumbs.create_' . e($what)), route('split.journal.create', [$what])); + $breadcrumbs->push(trans('breadcrumbs.edit_journal', ['description' => $journal->description]), route('transactions.split.edit', [$journal->id])); } ); diff --git a/app/Import/Converter/AccountId.php b/app/Import/Converter/AccountId.php index 5b39999f2e..4c07790fa5 100644 --- a/app/Import/Converter/AccountId.php +++ b/app/Import/Converter/AccountId.php @@ -40,7 +40,8 @@ class AccountId extends BasicConverter implements ConverterInterface return new Account; } /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [$this->user]); + $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]]); diff --git a/app/Import/Converter/Amount.php b/app/Import/Converter/Amount.php index 6b64a2b862..ab0fceb6b7 100644 --- a/app/Import/Converter/Amount.php +++ b/app/Import/Converter/Amount.php @@ -62,7 +62,7 @@ class Amount extends BasicConverter implements ConverterInterface $this->setCertainty(90); - return round(floatval($value), 4); + return round(floatval($value), 12); } } diff --git a/app/Import/Converter/AssetAccountIban.php b/app/Import/Converter/AssetAccountIban.php index 9ed749a781..d2336c282d 100644 --- a/app/Import/Converter/AssetAccountIban.php +++ b/app/Import/Converter/AssetAccountIban.php @@ -43,7 +43,8 @@ class AssetAccountIban extends BasicConverter implements ConverterInterface } /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [$this->user]); + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); if (isset($this->mapping[$value])) { diff --git a/app/Import/Converter/AssetAccountName.php b/app/Import/Converter/AssetAccountName.php index 4785ae16d2..8618d52f2d 100644 --- a/app/Import/Converter/AssetAccountName.php +++ b/app/Import/Converter/AssetAccountName.php @@ -43,7 +43,8 @@ class AssetAccountName extends BasicConverter implements ConverterInterface } /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [$this->user]); + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); if (isset($this->mapping[$value])) { diff --git a/app/Import/Converter/AssetAccountNumber.php b/app/Import/Converter/AssetAccountNumber.php index 5c71bf57f1..d389492c91 100644 --- a/app/Import/Converter/AssetAccountNumber.php +++ b/app/Import/Converter/AssetAccountNumber.php @@ -41,7 +41,8 @@ class AssetAccountNumber extends BasicConverter implements ConverterInterface } /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [$this->user]); + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); if (isset($this->mapping[$value])) { diff --git a/app/Import/Converter/BasicConverter.php b/app/Import/Converter/BasicConverter.php index bce8b5e46b..91183ba936 100644 --- a/app/Import/Converter/BasicConverter.php +++ b/app/Import/Converter/BasicConverter.php @@ -37,7 +37,7 @@ class BasicConverter /** * @return int */ - public function getCertainty():int + public function getCertainty(): int { return $this->certainty; } diff --git a/app/Import/Converter/BillId.php b/app/Import/Converter/BillId.php index 06713afaf4..cb20e4c7c9 100644 --- a/app/Import/Converter/BillId.php +++ b/app/Import/Converter/BillId.php @@ -42,7 +42,8 @@ class BillId extends BasicConverter implements ConverterInterface } /** @var BillRepositoryInterface $repository */ - $repository = app(BillRepositoryInterface::class, [$this->user]); + $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]]); diff --git a/app/Import/Converter/BillName.php b/app/Import/Converter/BillName.php index 6c4aa2fc17..3d2dbe9a71 100644 --- a/app/Import/Converter/BillName.php +++ b/app/Import/Converter/BillName.php @@ -44,7 +44,8 @@ class BillName extends BasicConverter implements ConverterInterface } /** @var BillRepositoryInterface $repository */ - $repository = app(BillRepositoryInterface::class, [$this->user]); + $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]]); diff --git a/app/Import/Converter/BudgetId.php b/app/Import/Converter/BudgetId.php index 0fe769f70e..cf709c0beb 100644 --- a/app/Import/Converter/BudgetId.php +++ b/app/Import/Converter/BudgetId.php @@ -42,7 +42,8 @@ class BudgetId extends BasicConverter implements ConverterInterface } /** @var BudgetRepositoryInterface $repository */ - $repository = app(BudgetRepositoryInterface::class, [$this->user]); + $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]]); diff --git a/app/Import/Converter/BudgetName.php b/app/Import/Converter/BudgetName.php index fc5d5416fc..7ecd85530c 100644 --- a/app/Import/Converter/BudgetName.php +++ b/app/Import/Converter/BudgetName.php @@ -42,7 +42,8 @@ class BudgetName extends BasicConverter implements ConverterInterface } /** @var BudgetRepositoryInterface $repository */ - $repository = app(BudgetRepositoryInterface::class, [$this->user]); + $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]]); diff --git a/app/Import/Converter/CategoryId.php b/app/Import/Converter/CategoryId.php index 5d538f4b90..2544a61597 100644 --- a/app/Import/Converter/CategoryId.php +++ b/app/Import/Converter/CategoryId.php @@ -42,7 +42,8 @@ class CategoryId extends BasicConverter implements ConverterInterface } /** @var CategoryRepositoryInterface $repository */ - $repository = app(CategoryRepositoryInterface::class, [$this->user]); + $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]]); diff --git a/app/Import/Converter/CategoryName.php b/app/Import/Converter/CategoryName.php index 28e7650c1f..f8af2414b1 100644 --- a/app/Import/Converter/CategoryName.php +++ b/app/Import/Converter/CategoryName.php @@ -42,7 +42,8 @@ class CategoryName extends BasicConverter implements ConverterInterface } /** @var CategoryRepositoryInterface $repository */ - $repository = app(CategoryRepositoryInterface::class, [$this->user]); + $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]]); diff --git a/app/Import/Converter/CurrencyCode.php b/app/Import/Converter/CurrencyCode.php index 48901cceaa..f78433ddf3 100644 --- a/app/Import/Converter/CurrencyCode.php +++ b/app/Import/Converter/CurrencyCode.php @@ -36,6 +36,7 @@ class CurrencyCode extends BasicConverter implements ConverterInterface /** @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]]); diff --git a/app/Import/Converter/CurrencyId.php b/app/Import/Converter/CurrencyId.php index 6622899264..299cde1bf3 100644 --- a/app/Import/Converter/CurrencyId.php +++ b/app/Import/Converter/CurrencyId.php @@ -42,7 +42,8 @@ class CurrencyId extends BasicConverter implements ConverterInterface } /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class, [$this->user]); + $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]]); diff --git a/app/Import/Converter/CurrencyName.php b/app/Import/Converter/CurrencyName.php index ad28107f4b..71af377582 100644 --- a/app/Import/Converter/CurrencyName.php +++ b/app/Import/Converter/CurrencyName.php @@ -42,7 +42,8 @@ class CurrencyName extends BasicConverter implements ConverterInterface } /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class, [$this->user]); + $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]]); diff --git a/app/Import/Converter/CurrencySymbol.php b/app/Import/Converter/CurrencySymbol.php index 307d409cd0..27ed50dd48 100644 --- a/app/Import/Converter/CurrencySymbol.php +++ b/app/Import/Converter/CurrencySymbol.php @@ -42,7 +42,8 @@ class CurrencySymbol extends BasicConverter implements ConverterInterface } /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class, [$this->user]); + $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]]); diff --git a/app/Import/Converter/OpposingAccountIban.php b/app/Import/Converter/OpposingAccountIban.php index c3dacbd712..ee8b40747c 100644 --- a/app/Import/Converter/OpposingAccountIban.php +++ b/app/Import/Converter/OpposingAccountIban.php @@ -43,7 +43,8 @@ class OpposingAccountIban extends BasicConverter implements ConverterInterface } /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [$this->user]); + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); if (isset($this->mapping[$value])) { diff --git a/app/Import/Converter/OpposingAccountName.php b/app/Import/Converter/OpposingAccountName.php index 4516873a25..fa51245fd6 100644 --- a/app/Import/Converter/OpposingAccountName.php +++ b/app/Import/Converter/OpposingAccountName.php @@ -42,7 +42,8 @@ class OpposingAccountName extends BasicConverter implements ConverterInterface } /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [$this->user]); + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); if (isset($this->mapping[$value])) { diff --git a/app/Import/Converter/OpposingAccountNumber.php b/app/Import/Converter/OpposingAccountNumber.php index 86df680223..d513a88ae0 100644 --- a/app/Import/Converter/OpposingAccountNumber.php +++ b/app/Import/Converter/OpposingAccountNumber.php @@ -43,7 +43,8 @@ class OpposingAccountNumber extends BasicConverter implements ConverterInterface } /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [$this->user]); + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); if (isset($this->mapping[$value])) { diff --git a/app/Import/Converter/TagSplit.php b/app/Import/Converter/TagSplit.php new file mode 100644 index 0000000000..d93074b7d0 --- /dev/null +++ b/app/Import/Converter/TagSplit.php @@ -0,0 +1,86 @@ +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 index d4e4687e3a..e9fbbaeecd 100644 --- a/app/Import/Converter/TagsComma.php +++ b/app/Import/Converter/TagsComma.php @@ -13,7 +13,6 @@ declare(strict_types = 1); namespace FireflyIII\Import\Converter; -use FireflyIII\Repositories\Tag\TagRepositoryInterface; use Illuminate\Support\Collection; use Log; @@ -41,49 +40,7 @@ class TagsComma extends BasicConverter implements ConverterInterface return new Collection; } $parts = array_unique(explode(',', $value)); - $set = new Collection; - Log::debug('Exploded parts.', $parts); - - /** @var TagRepositoryInterface $repository */ - $repository = app(TagRepositoryInterface::class, [$this->user]); - - - /** @var string $part */ - foreach ($parts as $part) { - if (isset($this->mapping[$part])) { - Log::debug('Found tag in mapping. Should exist.', ['value' => $part, 'map' => $this->mapping[$part]]); - $tag = $repository->find(intval($this->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); - } - } + $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 index d2d86eb8eb..3c437bd94b 100644 --- a/app/Import/Converter/TagsSpace.php +++ b/app/Import/Converter/TagsSpace.php @@ -13,7 +13,6 @@ declare(strict_types = 1); namespace FireflyIII\Import\Converter; -use FireflyIII\Repositories\Tag\TagRepositoryInterface; use Illuminate\Support\Collection; use Log; @@ -41,49 +40,7 @@ class TagsSpace extends BasicConverter implements ConverterInterface return new Collection; } $parts = array_unique(explode(' ', $value)); - $set = new Collection; - Log::debug('Exploded parts.', $parts); - - /** @var TagRepositoryInterface $repository */ - $repository = app(TagRepositoryInterface::class, [$this->user]); - - - /** @var string $part */ - foreach ($parts as $part) { - if (isset($this->mapping[$part])) { - Log::debug('Found tag in mapping. Should exist.', ['value' => $part, 'map' => $this->mapping[$part]]); - $tag = $repository->find(intval($this->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); - } - } + $set = TagSplit::createSetFromSplits($this->user, $this->mapping, $parts); $this->setCertainty(100); return $set; diff --git a/app/Import/ImportEntry.php b/app/Import/ImportEntry.php index cd3f48b402..fba65e99d4 100644 --- a/app/Import/ImportEntry.php +++ b/app/Import/ImportEntry.php @@ -96,6 +96,7 @@ class ImportEntry case 'account-id': case 'account-iban': case 'account-name': + case 'account-number': $this->setObject('asset-account', $convertedValue, $certainty); break; case 'opposing-number': diff --git a/app/Import/ImportProcedure.php b/app/Import/ImportProcedure.php index ae6b2cf910..991c5addcd 100644 --- a/app/Import/ImportProcedure.php +++ b/app/Import/ImportProcedure.php @@ -23,7 +23,7 @@ use Illuminate\Support\Collection; * * @package FireflyIII\Import */ -class ImportProcedure +class ImportProcedure implements ImportProcedureInterface { /** @@ -31,7 +31,7 @@ class ImportProcedure * * @return Collection */ - public static function runImport(ImportJob $job): Collection + public function runImport(ImportJob $job): Collection { // update job to say we started. $job->status = 'import_running'; @@ -58,7 +58,8 @@ class ImportProcedure if ($job->configuration['import-account'] != 0) { /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [$job->user]); + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($job->user); $validator->setDefaultImportAccount($repository->find($job->configuration['import-account'])); } diff --git a/app/Generator/Chart/PiggyBank/PiggyBankChartGeneratorInterface.php b/app/Import/ImportProcedureInterface.php similarity index 51% rename from app/Generator/Chart/PiggyBank/PiggyBankChartGeneratorInterface.php rename to app/Import/ImportProcedureInterface.php index 2841cd928e..22a519aa95 100644 --- a/app/Generator/Chart/PiggyBank/PiggyBankChartGeneratorInterface.php +++ b/app/Import/ImportProcedureInterface.php @@ -1,6 +1,6 @@ where('data', json_encode($hash))->first(['journal_meta.*']); + $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; @@ -155,7 +160,8 @@ class ImportStorage private function createImportTag(): Tag { /** @var TagRepositoryInterface $repository */ - $repository = app(TagRepositoryInterface::class, [$this->user]); + $repository = app(TagRepositoryInterface::class); + $repository->setUser($this->user); $data = [ 'tag' => trans('firefly.import_with_key', ['key' => $this->job->key]), 'date' => new Carbon, @@ -192,21 +198,6 @@ class ImportStorage } - /** - * @param float $amount - * - * @return string - */ - private function makePositive(float $amount): string - { - $amount = strval($amount); - if (bccomp($amount, '0', 4) === -1) { // left is larger than right - $amount = bcmul($amount, '-1'); - } - - return $amount; - } - /** * @param $entry * @@ -365,21 +356,21 @@ class ImportStorage $journal = $this->storeJournal($entry); - $amount = $this->makePositive($entry->fields['amount']); + $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' => $journal->description, + 'description' => null, 'amount' => bcmul($amount, '-1'), ]; $destinationData = [ 'account_id' => $accounts['destination']->id, 'transaction_journal_id' => $journal->id, - 'description' => $journal->description, + 'description' => null, 'amount' => $amount, ]; diff --git a/app/Import/ImportValidator.php b/app/Import/ImportValidator.php index 20705ca605..9a5a254151 100644 --- a/app/Import/ImportValidator.php +++ b/app/Import/ImportValidator.php @@ -177,7 +177,8 @@ class ImportValidator // find it first by new type: /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [$this->user]); + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); $result = $repository->findByName($account->name, [$type]); if (is_null($result->id)) { @@ -214,7 +215,8 @@ class ImportValidator { /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [$this->user]); + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); $name = 'Unknown expense account'; $result = $repository->findByName($name, [AccountType::EXPENSE]); @@ -235,7 +237,8 @@ class ImportValidator { /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [$this->user]); + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); $name = 'Unknown revenue account'; $result = $repository->findByName($name, [AccountType::REVENUE]); @@ -382,7 +385,8 @@ class ImportValidator { if (is_null($entry->fields['currency'])) { /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class, [$this->user]); + $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; @@ -416,6 +420,7 @@ class ImportValidator 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.'); diff --git a/app/Import/Setup/CsvSetup.php b/app/Import/Setup/CsvSetup.php index a49c5fe070..3d0fc10ed3 100644 --- a/app/Import/Setup/CsvSetup.php +++ b/app/Import/Setup/CsvSetup.php @@ -112,14 +112,16 @@ class CsvSetup implements SetupInterface */ 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; @@ -179,8 +181,7 @@ class CsvSetup implements SetupInterface public function saveImportConfiguration(array $data, FileBag $files): bool { /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class, [auth()->user()]); - + $repository = app(AccountRepositoryInterface::class); $importId = $data['csv_import_account'] ?? 0; $account = $repository->find(intval($importId)); @@ -323,6 +324,7 @@ class CsvSetup implements SetupInterface foreach ($config['column-do-mapping'] as $index => $mustBeMapped) { if ($mustBeMapped) { + $column = $config['column-roles'][$index] ?? '_ignore'; // is valid column? @@ -426,14 +428,18 @@ class CsvSetup implements SetupInterface } /** + * This method collects the data that will enable a user to choose column content. + * * @return array */ - private function getDataForColumnRoles():array + private function getDataForColumnRoles(): array { + Log::debug('Now in getDataForColumnRoles()'); $config = $this->job->configuration; $data = [ - 'columns' => [], - 'columnCount' => 0, + 'columns' => [], + 'columnCount' => 0, + 'columnHeaders' => [], ]; // show user column role configuration. @@ -442,25 +448,32 @@ class CsvSetup implements SetupInterface // create CSV reader. $reader = Reader::createFromString($content); $reader->setDelimiter($config['delimiter']); - $start = $config['has-headers'] ? 1 : 0; - $end = $start + config('csv.example_rows'); + $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); + $value = trim($value); + $data['columnHeaders'][$index] = $header[$index] ?? ''; if (strlen($value) > 0) { $data['columns'][$index][] = $value; } diff --git a/app/Import/Specifics/RabobankDescription.php b/app/Import/Specifics/RabobankDescription.php index a3988f2a61..59ff2abb53 100644 --- a/app/Import/Specifics/RabobankDescription.php +++ b/app/Import/Specifics/RabobankDescription.php @@ -45,9 +45,11 @@ class RabobankDescription implements SpecificInterface */ public function run(array $row): array { - $oppositeAccount = trim($row[5]); - $oppositeName = trim($row[6]); - $alternateName = trim($row[10]); + Log::debug(sprintf('Now in RabobankSpecific::run(). Row has %d columns', count($row))); + $oppositeAccount = isset($row[5]) ? trim($row[5]) : ''; + $oppositeName = isset($row[6]) ? trim($row[6]) : ''; + $alternateName = isset($row[10]) ? trim($row[10]) : ''; + if (strlen($oppositeAccount) < 1 && strlen($oppositeName) < 1) { Log::debug( sprintf( diff --git a/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php b/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php index 8048fe9cd7..0ac899ffcb 100644 --- a/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php +++ b/app/Jobs/ExecuteRuleGroupOnExistingTransactions.php @@ -14,8 +14,8 @@ declare(strict_types = 1); namespace FireflyIII\Jobs; use Carbon\Carbon; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Models\RuleGroup; -use FireflyIII\Repositories\Journal\JournalTaskerInterface; use FireflyIII\Rules\Processor; use FireflyIII\User; use Illuminate\Contracts\Queue\ShouldQueue; @@ -129,16 +129,16 @@ class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue public function handle() { // Lookup all journals that match the parameters specified - $journals = $this->collectJournals(); + $transactions = $this->collectJournals(); // Find processors for each rule within the current rule group $processors = $this->collectProcessors(); // Execute the rules for each transaction - foreach ($journals as $journal) { + foreach ($transactions as $transaction) { /** @var Processor $processor */ foreach ($processors as $processor) { - $processor->handleTransactionJournal($journal); + $processor->handleTransaction($transaction); // Stop processing this group if the rule specifies 'stop_processing' if ($processor->getRule()->stop_processing) { @@ -155,10 +155,12 @@ class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue */ protected function collectJournals() { - /** @var JournalTaskerInterface $tasker */ - $tasker = app(JournalTaskerInterface::class); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setAccounts($this->accounts)->setRange($this->startDate, $this->endDate); - return $tasker->getJournalsInRange($this->accounts, $this->startDate, $this->endDate); + return $collector->getJournals(); } /** diff --git a/app/Jobs/Job.php b/app/Jobs/Job.php index b5c465e455..92fa2f3246 100644 --- a/app/Jobs/Job.php +++ b/app/Jobs/Job.php @@ -22,16 +22,6 @@ use Illuminate\Bus\Queueable; */ abstract class Job { - /* - |-------------------------------------------------------------------------- - | Queueable Jobs - |-------------------------------------------------------------------------- - | - | This job base class provides a central location to place any logic that - | is shared across all of your jobs. The trait included with the class - | provides access to the "onQueue" and "delay" queue helper methods. - | - */ use Queueable; } diff --git a/app/Jobs/MailError.php b/app/Jobs/MailError.php index a0974e694b..44e1ec89b5 100644 --- a/app/Jobs/MailError.php +++ b/app/Jobs/MailError.php @@ -78,10 +78,10 @@ class MailError extends Job implements ShouldQueue $args['ip'] = $this->ipAddress; Mail::send( - ['emails.error-html', 'emails.error'], $args, + ['emails.error-html', 'emails.error-text'], $args, function (Message $message) use ($email) { if ($email != 'mail@example.com') { - $message->to($email, $email)->subject('Caught an error in Firely III.'); + $message->to($email, $email)->subject('Caught an error in Firely III'); } } ); diff --git a/app/Models/Account.php b/app/Models/Account.php index d9325d1dea..11b337ba10 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -26,57 +26,35 @@ use Illuminate\Database\Query\JoinClause; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Watson\Validating\ValidatingTrait; + /** - * FireflyIII\Models\Account + * Class Account * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property \Carbon\Carbon $deleted_at - * @property integer $user_id - * @property integer $account_type_id - * @property string $name - * @property boolean $active - * @property boolean $encrypted - * @property float $virtual_balance - * @property string $iban - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\AccountMeta[] $accountMeta - * @property-read \FireflyIII\Models\AccountType $accountType - * @property-read mixed $name_for_editform - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\PiggyBank[] $piggyBanks - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Transaction[] $transactions - * @property-read \FireflyIII\User $user - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account accountTypeIn($types) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account hasMetaValue($name, $value) - * @property string $startBalance - * @property string $endBalance - * @property float $difference - * @property \Carbon\Carbon $lastActivityDate - * @property float $piggyBalance - * @property float $percentage - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account whereAccountTypeId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account whereName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account whereActive($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account whereEncrypted($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account whereVirtualBalance($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Account whereIban($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class Account extends Model { use SoftDeletes, ValidatingTrait; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'active' => 'boolean', + 'encrypted' => 'boolean', + ]; /** @var array */ protected $dates = ['created_at', 'updated_at', 'deleted_at']; /** @var array */ protected $fillable = ['user_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban']; /** @var array */ - protected $hidden = ['virtual_balance_encrypted', 'encrypted']; + protected $hidden = ['encrypted']; protected $rules = [ 'user_id' => 'required|exists:users,id', @@ -91,12 +69,16 @@ class Account extends Model /** * @param array $fields * - * @return Account|null + * @return Account + * @throws FireflyException */ public static function firstOrCreateEncrypted(array $fields) { + if (!isset($fields['user_id'])) { + throw new FireflyException('Missing required field "user_id".'); + } // everything but the name: - $query = Account::orderBy('id'); + $query = self::orderBy('id'); $search = $fields; unset($search['name'], $search['iban']); @@ -104,19 +86,22 @@ class Account extends Model $query->where($name, $value); } $set = $query->get(['accounts.*']); + + // account must have a name. If not set, use IBAN. + if (!isset($fields['name'])) { + $fields['name'] = $fields['iban']; + } + + /** @var Account $account */ foreach ($set as $account) { if ($account->name == $fields['name']) { return $account; } } - // account must have a name. If not set, use IBAN. - if (!isset($fields['name'])) { - $fields['name'] = $fields['iban']; - } // create it! - $account = Account::create($fields); + $account = self::create($fields); return $account; @@ -154,6 +139,20 @@ class Account extends Model return $this->belongsTo('FireflyIII\Models\AccountType'); } + /** + * @return string + */ + public function getEditNameAttribute(): string + { + $name = $this->name; + + if ($this->accountType->type === AccountType::CASH) { + return ''; + } + + return $name; + } + /** * FIxxME can return null * @@ -189,7 +188,7 @@ class Account extends Model { foreach ($this->accountMeta as $meta) { if ($meta->name == $fieldName) { - return $meta->data; + return strval($meta->data); } } @@ -205,7 +204,7 @@ class Account extends Model public function getNameAttribute($value): string { - if (intval($this->encrypted) == 1) { + if ($this->encrypted) { return Crypt::decrypt($value); } @@ -220,12 +219,11 @@ class Account extends Model */ public function getOpeningBalanceAmount(): string { - $journal = TransactionJournal - ::sortCorrectly() - ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transactions.account_id', $this->id) - ->transactionTypes([TransactionType::OPENING_BALANCE]) - ->first(['transaction_journals.*']); + $journal = TransactionJournal::sortCorrectly() + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $this->id) + ->transactionTypes([TransactionType::OPENING_BALANCE]) + ->first(['transaction_journals.*']); if (is_null($journal)) { return '0'; } @@ -251,12 +249,11 @@ class Account extends Model public function getOpeningBalanceDate(): Carbon { $date = new Carbon('1900-01-01'); - $journal = TransactionJournal - ::sortCorrectly() - ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transactions.account_id', $this->id) - ->transactionTypes([TransactionType::OPENING_BALANCE]) - ->first(['transaction_journals.*']); + $journal = TransactionJournal::sortCorrectly() + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $this->id) + ->transactionTypes([TransactionType::OPENING_BALANCE]) + ->first(['transaction_journals.*']); if (is_null($journal)) { return $date; } @@ -318,8 +315,9 @@ class Account extends Model */ public function setNameAttribute($value) { - $this->attributes['name'] = $value; - $this->attributes['encrypted'] = false; + $encrypt = config('firefly.encryption'); + $this->attributes['name'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['encrypted'] = $encrypt; } /** @@ -328,7 +326,7 @@ class Account extends Model */ public function setVirtualBalanceAttribute($value) { - $this->attributes['virtual_balance'] = strval(round($value, 2)); + $this->attributes['virtual_balance'] = strval(round($value, 12)); } /** diff --git a/app/Models/AccountMeta.php b/app/Models/AccountMeta.php index c6a2c6004a..48953f09d5 100644 --- a/app/Models/AccountMeta.php +++ b/app/Models/AccountMeta.php @@ -17,26 +17,24 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; /** - * FireflyIII\Models\AccountMeta + * Class AccountMeta * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $account_id - * @property string $name - * @property string $data - * @property-read Account $account - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\AccountMeta whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\AccountMeta whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\AccountMeta whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\AccountMeta whereAccountId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\AccountMeta whereName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\AccountMeta whereData($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class AccountMeta extends Model { + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + ]; + /** @var array */ protected $dates = ['created_at', 'updated_at']; protected $fillable = ['account_id', 'name', 'data']; protected $table = 'account_meta'; diff --git a/app/Models/AccountType.php b/app/Models/AccountType.php index 920b62a950..3efa588ada 100644 --- a/app/Models/AccountType.php +++ b/app/Models/AccountType.php @@ -17,18 +17,9 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; /** - * FireflyIII\Models\AccountType + * Class AccountType * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property string $type - * @property-read \Illuminate\Database\Eloquent\Collection|Account[] $accounts - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\AccountType whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\AccountType whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\AccountType whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\AccountType whereType($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class AccountType extends Model { @@ -42,6 +33,18 @@ class AccountType extends Model const IMPORT = 'Import account'; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + ]; + + /** @var array */ protected $dates = ['created_at', 'updated_at']; // diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php index 85d21c16c8..36599f3d6b 100644 --- a/app/Models/Attachment.php +++ b/app/Models/Attachment.php @@ -21,46 +21,29 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** - * FireflyIII\Models\Attachment + * Class Attachment * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property string $deleted_at - * @property integer $attachable_id - * @property string $attachable_type - * @property integer $user_id - * @property string $md5 - * @property string $filename - * @property string $title - * @property string $description - * @property string $notes - * @property string $mime - * @property integer $size - * @property boolean $uploaded - * @property-read Attachment $attachable - * @property-read \FireflyIII\User $user - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereAttachableId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereAttachableType($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereMd5($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereFilename($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereTitle($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereDescription($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereNotes($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereMime($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereSize($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Attachment whereUploaded($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class Attachment extends Model { use SoftDeletes; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'uploaded' => 'boolean', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at', 'deleted_at']; + /** @var array */ protected $fillable = ['attachable_id', 'attachable_type', 'user_id', 'md5', 'filename', 'mime', 'title', 'notes', 'description', 'size', 'uploaded']; /** diff --git a/app/Models/AvailableBudget.php b/app/Models/AvailableBudget.php new file mode 100644 index 0000000000..d51d8b0418 --- /dev/null +++ b/app/Models/AvailableBudget.php @@ -0,0 +1,62 @@ + 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'start_date' => 'date', + 'end_date' => 'date', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at', 'deleted_at']; + /** @var array */ + protected $fillable = ['user_id', 'transaction_currency_id', 'amount', 'start_date', 'end_date']; + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function transactionCurrency() + { + return $this->belongsTo('FireflyIII\Models\TransactionCurrency'); + } + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo('FireflyIII\User'); + } +} diff --git a/app/Models/Bill.php b/app/Models/Bill.php index d13f6d372e..54a7429746 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -21,55 +21,33 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Watson\Validating\ValidatingTrait; /** - * FireflyIII\Models\Bill + * Class Bill * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $user_id - * @property string $name - * @property string $match - * @property float $amount_min - * @property float $amount_max - * @property \Carbon\Carbon $date - * @property boolean $active - * @property boolean $automatch - * @property string $repeat_freq - * @property integer $skip - * @property boolean $name_encrypted - * @property boolean $match_encrypted - * @property-read \Illuminate\Database\Eloquent\Collection|TransactionJournal[] $transactionjournals - * @property-read \FireflyIII\User $user - * @property \Carbon\Carbon $nextExpectedMatch - * @property \Carbon\Carbon $lastFoundMatch - * @property bool $paidInPeriod - * @property string $lastPaidAmount - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereMatch($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereAmountMin($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereAmountMax($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereDate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereActive($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereAutomatch($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereRepeatFreq($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereSkip($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereNameEncrypted($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereMatchEncrypted($value) - * @mixin \Eloquent - * @property string $deleted_at - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Bill whereDeletedAt($value) - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournal[] $transactionJournals + * @package FireflyIII\Models */ class Bill extends Model { use ValidatingTrait; - - protected $dates = ['created_at', 'updated_at', 'date']; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'date' => 'date', + 'skip' => 'int', + 'automatch' => 'boolean', + 'active' => 'boolean', + 'name_encrypted' => 'boolean', + 'match_encrypted' => 'boolean', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at', 'deleted_at']; protected $fillable = ['name', 'match', 'amount_min', 'match_encrypted', 'name_encrypted', 'user_id', 'amount_max', 'date', 'repeat_freq', 'skip', 'automatch', 'active',]; @@ -126,7 +104,7 @@ class Bill extends Model */ public function setAmountMaxAttribute($value) { - $this->attributes['amount_max'] = strval(round($value, 2)); + $this->attributes['amount_max'] = strval(round($value, 12)); } /** @@ -134,7 +112,7 @@ class Bill extends Model */ public function setAmountMinAttribute($value) { - $this->attributes['amount_min'] = strval(round($value, 2)); + $this->attributes['amount_min'] = strval(round($value, 12)); } /** @@ -142,8 +120,9 @@ class Bill extends Model */ public function setMatchAttribute($value) { - $this->attributes['match'] = Crypt::encrypt($value); - $this->attributes['match_encrypted'] = true; + $encrypt = config('firefly.encryption'); + $this->attributes['match'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['match_encrypted'] = $encrypt; } /** @@ -151,8 +130,9 @@ class Bill extends Model */ public function setNameAttribute($value) { - $this->attributes['name'] = Crypt::encrypt($value); - $this->attributes['name_encrypted'] = true; + $encrypt = config('firefly.encryption'); + $this->attributes['name'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['name_encrypted'] = $encrypt; } /** diff --git a/app/Models/Budget.php b/app/Models/Budget.php index db69a0da47..504802e05a 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -21,42 +21,30 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Watson\Validating\ValidatingTrait; /** - * FireflyIII\Models\Budget + * Class Budget * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property \Carbon\Carbon $deleted_at - * @property string $name - * @property integer $user_id - * @property boolean $active - * @property boolean $encrypted - * @property-read \Illuminate\Database\Eloquent\Collection|BudgetLimit[] $budgetlimits - * @property-read \Illuminate\Database\Eloquent\Collection|TransactionJournal[] $transactionjournals - * @property-read \FireflyIII\User $user - * @property string $dateFormatted - * @property string $budgeted - * @property float $amount - * @property \Carbon\Carbon $date - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Budget whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Budget whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Budget whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Budget whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Budget whereName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Budget whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Budget whereActive($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Budget whereEncrypted($value) - * @mixin \Eloquent - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Transaction[] $transactions - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\LimitRepetition[] $limitrepetitions - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournal[] $transactionJournals + * @package FireflyIII\Models */ class Budget extends Model { use SoftDeletes, ValidatingTrait; - protected $dates = ['created_at', 'updated_at', 'deleted_at', 'startdate', 'enddate']; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'active' => 'boolean', + 'encrypted' => 'boolean', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at', 'deleted_at']; protected $fillable = ['user_id', 'name', 'active']; protected $hidden = ['encrypted']; protected $rules = ['name' => 'required|between:1,200',]; @@ -69,7 +57,7 @@ class Budget extends Model public static function firstOrCreateEncrypted(array $fields) { // everything but the name: - $query = Budget::orderBy('id'); + $query = self::orderBy('id'); $search = $fields; unset($search['name']); foreach ($search as $name => $value) { @@ -83,7 +71,7 @@ class Budget extends Model } } // create it! - $budget = Budget::create($fields); + $budget = self::create($fields); return $budget; @@ -121,28 +109,21 @@ class Budget extends Model public function getNameAttribute($value) { - if (intval($this->encrypted) == 1) { + if ($this->encrypted) { return Crypt::decrypt($value); } return $value; } - /** - * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough - */ - public function limitrepetitions() - { - return $this->hasManyThrough('FireflyIII\Models\LimitRepetition', 'FireflyIII\Models\BudgetLimit', 'budget_id'); - } - /** * @param $value */ public function setNameAttribute($value) { - $this->attributes['name'] = Crypt::encrypt($value); - $this->attributes['encrypted'] = true; + $encrypt = config('firefly.encryption'); + $this->attributes['name'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['encrypted'] = $encrypt; } /** diff --git a/app/Models/BudgetLimit.php b/app/Models/BudgetLimit.php index fa1ab82f95..a6c2b68f97 100644 --- a/app/Models/BudgetLimit.php +++ b/app/Models/BudgetLimit.php @@ -14,36 +14,50 @@ declare(strict_types = 1); namespace FireflyIII\Models; use Illuminate\Database\Eloquent\Model; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** - * FireflyIII\Models\BudgetLimit + * Class BudgetLimit * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $budget_id - * @property \Carbon\Carbon $startdate - * @property float $amount - * @property boolean $repeats - * @property string $repeat_freq - * @property-read Budget $budget - * @property int $component_id - * @property-read \Illuminate\Database\Eloquent\Collection|LimitRepetition[] $limitrepetitions - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\BudgetLimit whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\BudgetLimit whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\BudgetLimit whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\BudgetLimit whereBudgetId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\BudgetLimit whereStartdate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\BudgetLimit whereAmount($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\BudgetLimit whereRepeats($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\BudgetLimit whereRepeatFreq($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class BudgetLimit extends Model { - protected $dates = ['created_at', 'updated_at', 'startdate']; - protected $hidden = ['amount_encrypted']; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'start_date' => 'date', + 'end_date' => 'date', + 'repeats' => 'boolean', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at', 'start_date', 'end_date']; + + /** + * @param $value + * + * @return mixed + */ + public static function routeBinder($value) + { + if (auth()->check()) { + $object = self::where('budget_limits.id', $value) + ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') + ->where('budgets.user_id', auth()->user()->id) + ->first(['budget_limits.*']); + if ($object) { + return $object; + } + } + throw new NotFoundHttpException; + } /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo @@ -61,12 +75,13 @@ class BudgetLimit extends Model return $this->hasMany('FireflyIII\Models\LimitRepetition'); } + /** * @param $value */ public function setAmountAttribute($value) { - $this->attributes['amount'] = strval(round($value, 2)); + $this->attributes['amount'] = strval(round($value, 12)); } } diff --git a/app/Models/Category.php b/app/Models/Category.php index c51dabc025..50349e9b44 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -21,40 +21,34 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Watson\Validating\ValidatingTrait; /** - * FireflyIII\Models\Category + * Class Category * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property \Carbon\Carbon $deleted_at - * @property string $name - * @property integer $user_id - * @property boolean $encrypted - * @property-read \Illuminate\Database\Eloquent\Collection|TransactionJournal[] $transactionjournals - * @property-read \FireflyIII\User $user - * @property string $dateFormatted - * @property string $spent - * @property \Carbon\Carbon $lastActivity - * @property string $type - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Category whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Category whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Category whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Category whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Category whereName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Category whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Category whereEncrypted($value) - * @mixin \Eloquent - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Transaction[] $transactions - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournal[] $transactionJournals + * @package FireflyIII\Models */ class Category extends Model { use SoftDeletes, ValidatingTrait; - protected $dates = ['created_at', 'updated_at', 'deleted_at']; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'encrypted' => 'boolean', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at', 'deleted_at']; + /** @var array */ protected $fillable = ['user_id', 'name']; - protected $hidden = ['encrypted']; - protected $rules = ['name' => 'required|between:1,200',]; + /** @var array */ + protected $hidden = ['encrypted']; + /** @var array */ + protected $rules = ['name' => 'required|between:1,200',]; /** * @param array $fields @@ -64,7 +58,7 @@ class Category extends Model public static function firstOrCreateEncrypted(array $fields) { // everything but the name: - $query = Category::orderBy('id'); + $query = self::orderBy('id'); $search = $fields; unset($search['name']); foreach ($search as $name => $value) { @@ -78,7 +72,7 @@ class Category extends Model } } // create it! - $category = Category::create($fields); + $category = self::create($fields); return $category; @@ -108,7 +102,7 @@ class Category extends Model public function getNameAttribute($value) { - if (intval($this->encrypted) == 1) { + if ($this->encrypted) { return Crypt::decrypt($value); } @@ -121,8 +115,9 @@ class Category extends Model */ public function setNameAttribute($value) { - $this->attributes['name'] = Crypt::encrypt($value); - $this->attributes['encrypted'] = true; + $encrypt = config('firefly.encryption'); + $this->attributes['name'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['encrypted'] = $encrypt; } /** diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index d74eb7255d..b5edf6aced 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -17,26 +17,25 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; /** - * FireflyIII\Models\Configuration + * Class Configuration * - * @mixin \Eloquent - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property \Carbon\Carbon $deleted_at - * @property string $name - * @property string $data - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Configuration whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Configuration whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Configuration whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Configuration whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Configuration whereName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Configuration whereData($value) + * @package FireflyIII\Models */ class Configuration extends Model { use SoftDeletes; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + ]; + /** @var array */ protected $dates = ['created_at', 'updated_at', 'deleted_at']; protected $table = 'configuration'; diff --git a/app/Models/ExportJob.php b/app/Models/ExportJob.php index 4e9997abe1..a0eb58cfed 100644 --- a/app/Models/ExportJob.php +++ b/app/Models/ExportJob.php @@ -19,24 +19,19 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class ExportJob * - * @package FireflyIII - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $user_id - * @property string $key - * @property string $status - * @property-read \FireflyIII\User $user - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ExportJob whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ExportJob whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ExportJob whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ExportJob whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ExportJob whereKey($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ExportJob whereStatus($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class ExportJob extends Model { + /** @var array */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at']; + /** * @param $value * diff --git a/app/Models/ImportJob.php b/app/Models/ImportJob.php index c7ba8c4427..b8a7bd27c3 100644 --- a/app/Models/ImportJob.php +++ b/app/Models/ImportJob.php @@ -15,37 +15,31 @@ namespace FireflyIII\Models; use Crypt; use Illuminate\Database\Eloquent\Model; +use Log; use Storage; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - /** - * FireflyIII\Models\ImportJob + * Class ImportJob * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $user_id - * @property string $key - * @property string $file_type - * @property string $status - * @property array $configuration - * @property-read \FireflyIII\User $user - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ImportJob whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ImportJob whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ImportJob whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ImportJob whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ImportJob whereKey($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ImportJob whereFileType($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ImportJob whereStatus($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ImportJob whereConfiguration($value) - * @mixin \Eloquent - * @property string $extended_status - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\ImportJob whereExtendedStatus($value) + * @package FireflyIII\Models */ class ImportJob extends Model { + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at']; + protected $validStatus = [ 'import_status_never_started', // initial state @@ -171,6 +165,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)); return $content; } diff --git a/app/Models/LimitRepetition.php b/app/Models/LimitRepetition.php index 9d2dc5b416..d4f5b5f3dc 100644 --- a/app/Models/LimitRepetition.php +++ b/app/Models/LimitRepetition.php @@ -19,32 +19,26 @@ use Illuminate\Database\Eloquent\Model; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** - * FireflyIII\Models\LimitRepetition + * Class LimitRepetition * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $budget_limit_id - * @property \Carbon\Carbon $startdate - * @property \Carbon\Carbon $enddate - * @property float $amount - * @property-read BudgetLimit $budgetLimit - * @property int $budget_id - * @property string $spent - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\LimitRepetition whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\LimitRepetition whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\LimitRepetition whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\LimitRepetition whereBudgetLimitId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\LimitRepetition whereStartdate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\LimitRepetition whereEnddate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\LimitRepetition whereAmount($value) - * @mixin \Eloquent - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\LimitRepetition after($date) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\LimitRepetition before($date) + * @deprecated + * @package FireflyIII\Models */ class LimitRepetition extends Model { + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'startdate' => 'date', + 'enddate' => 'date', + ]; protected $dates = ['created_at', 'updated_at', 'startdate', 'enddate']; protected $hidden = ['amount_encrypted']; @@ -56,11 +50,11 @@ class LimitRepetition extends Model public static function routeBinder($value) { if (auth()->check()) { - $object = LimitRepetition::where('limit_repetitions.id', $value) - ->leftJoin('budget_limits', 'budget_limits.id', '=', 'limit_repetitions.budget_limit_id') - ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') - ->where('budgets.user_id', auth()->user()->id) - ->first(['limit_repetitions.*']); + $object = self::where('limit_repetitions.id', $value) + ->leftJoin('budget_limits', 'budget_limits.id', '=', 'limit_repetitions.budget_limit_id') + ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') + ->where('budgets.user_id', auth()->user()->id) + ->first(['limit_repetitions.*']); if ($object) { return $object; } diff --git a/app/Models/Note.php b/app/Models/Note.php index c811fcc7cd..55ee792ce9 100644 --- a/app/Models/Note.php +++ b/app/Models/Note.php @@ -16,43 +16,34 @@ namespace FireflyIII\Models; use Illuminate\Database\Eloquent\Model; use League\CommonMark\CommonMarkConverter; - /** - * FireflyIII\Models\Note + * Class Note * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property string $deleted_at - * @property integer $noteable_id - * @property string $noteable_type - * @property string $title - * @property string $text - * @property-read \Illuminate\Database\Eloquent\Model|\Eloquent $noteable - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Note whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Note whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Note whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Note whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Note whereNoteableId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Note whereNoteableType($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Note whereTitle($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Note whereText($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class Note extends Model { + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + ]; protected $dates = ['created_at', 'updated_at', 'deleted_at']; protected $fillable = ['title', 'text']; - /** - * @param $value - * * @return string */ public function getMarkdownAttribute(): string { - $converter = new CommonMarkConverter; + $converter = new CommonMarkConverter; + return $converter->convertToHtml($this->text); } diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index 3d5e5da150..d5b2b00187 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -21,51 +21,33 @@ use Steam; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** - * FireflyIII\Models\PiggyBank + * Class PiggyBank * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property \Carbon\Carbon $deleted_at - * @property integer $account_id - * @property string $name - * @property float $targetamount - * @property \Carbon\Carbon $startdate - * @property \Carbon\Carbon $targetdate - * @property integer $order - * @property boolean $encrypted - * @property-read Account $account - * @property-read \Illuminate\Database\Eloquent\Collection|PiggyBankRepetition[] $piggyBankRepetitions - * @property-read \Illuminate\Database\Eloquent\Collection|PiggyBankEvent[] $piggyBankEvents - * @property string $reminder - * @property PiggyBankRepetition $currentRep - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereAccountId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereTargetamount($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereStartdate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereTargetdate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereReminder($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereReminderSkip($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereRemindMe($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereOrder($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereEncrypted($value) - * @mixin \Eloquent - * @property boolean $active - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBank whereActive($value) - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Note[] $notes + * @package FireflyIII\Models */ class PiggyBank extends Model { use SoftDeletes; - protected $dates = ['created_at', 'updated_at', 'deleted_at', 'startdate', 'targetdate']; - protected $fillable - = ['name', 'account_id', 'order', 'targetamount', 'startdate', 'targetdate']; - protected $hidden = ['targetamount_encrypted', 'encrypted']; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'startdate' => 'date', + 'targetdate' => 'date', + 'order' => 'int', + 'active' => 'boolean', + 'encrypted' => 'boolean', + ]; + protected $dates = ['created_at', 'updated_at', 'deleted_at', 'startdate', 'targetdate']; + protected $fillable = ['name', 'account_id', 'order', 'targetamount', 'startdate', 'targetdate']; + protected $hidden = ['targetamount_encrypted', 'encrypted']; /** * @param PiggyBank $value @@ -119,7 +101,7 @@ class PiggyBank extends Model public function getNameAttribute($value) { - if (intval($this->encrypted) == 1) { + if ($this->encrypted) { return Crypt::decrypt($value); } @@ -177,8 +159,9 @@ class PiggyBank extends Model */ public function setNameAttribute($value) { - $this->attributes['name'] = Crypt::encrypt($value); - $this->attributes['encrypted'] = true; + $encrypt = config('firefly.encryption'); + $this->attributes['name'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['encrypted'] = $encrypt; } /** @@ -186,6 +169,6 @@ class PiggyBank extends Model */ public function setTargetamountAttribute($value) { - $this->attributes['targetamount'] = strval(round($value, 2)); + $this->attributes['targetamount'] = strval(round($value, 12)); } } diff --git a/app/Models/PiggyBankEvent.php b/app/Models/PiggyBankEvent.php index 5e959c66c9..a84383100d 100644 --- a/app/Models/PiggyBankEvent.php +++ b/app/Models/PiggyBankEvent.php @@ -16,29 +16,24 @@ namespace FireflyIII\Models; use Illuminate\Database\Eloquent\Model; /** - * FireflyIII\Models\PiggyBankEvent + * Class PiggyBankEvent * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $piggy_bank_id - * @property integer $transaction_journal_id - * @property \Carbon\Carbon $date - * @property float $amount - * @property PiggyBank $piggyBank - * @property-read TransactionJournal $transactionJournal - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankEvent whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankEvent whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankEvent whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankEvent wherePiggyBankId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankEvent whereTransactionJournalId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankEvent whereDate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankEvent whereAmount($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class PiggyBankEvent extends Model { + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'date' => 'date', + ]; protected $dates = ['created_at', 'updated_at', 'date']; protected $fillable = ['piggy_bank_id', 'transaction_journal_id', 'date', 'amount']; protected $hidden = ['amount_encrypted']; diff --git a/app/Models/PiggyBankRepetition.php b/app/Models/PiggyBankRepetition.php index 57aff06279..00c8221582 100644 --- a/app/Models/PiggyBankRepetition.php +++ b/app/Models/PiggyBankRepetition.php @@ -18,33 +18,28 @@ use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Model; /** - * FireflyIII\Models\PiggyBankRepetition + * Class PiggyBankRepetition * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $piggy_bank_id - * @property \Carbon\Carbon $startdate - * @property \Carbon\Carbon $targetdate - * @property float $currentamount - * @property-read PiggyBank $piggyBank - * @method static \Illuminate\Database\Query\Builder|PiggyBankRepetition onDates($start, $target) - * @method static \Illuminate\Database\Query\Builder|PiggyBankRepetition relevantOnDate($date) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankRepetition whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankRepetition whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankRepetition whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankRepetition wherePiggyBankId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankRepetition whereStartdate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankRepetition whereTargetdate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\PiggyBankRepetition whereCurrentamount($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class PiggyBankRepetition extends Model { + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'startdate' => 'date', + 'targetdate' => 'date', + ]; protected $dates = ['created_at', 'updated_at', 'startdate', 'targetdate']; protected $fillable = ['piggy_bank_id', 'startdate', 'targetdate', 'currentamount']; - protected $hidden = ['currentamount_encrypted']; /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo @@ -94,7 +89,7 @@ class PiggyBankRepetition extends Model */ public function setCurrentamountAttribute($value) { - $this->attributes['currentamount'] = strval(round($value, 2)); + $this->attributes['currentamount'] = strval(round($value, 12)); } } diff --git a/app/Models/Preference.php b/app/Models/Preference.php index 7a843e2470..f5a81dd909 100644 --- a/app/Models/Preference.php +++ b/app/Models/Preference.php @@ -20,29 +20,23 @@ use Illuminate\Database\Eloquent\Model; use Log; /** - * FireflyIII\Models\Preference + * Class Preference * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $user_id - * @property string $name - * @property string $name_encrypted - * @property string $data - * @property-read \FireflyIII\User $user - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Preference whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Preference whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Preference whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Preference whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Preference whereName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Preference whereNameEncrypted($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Preference whereData($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Preference whereDataEncrypted($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class Preference extends Model { + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + ]; protected $dates = ['created_at', 'updated_at']; protected $fillable = ['user_id', 'data', 'name', 'data']; @@ -64,7 +58,7 @@ class Preference extends Model } - return json_decode($data); + return json_decode($data, true); } /** diff --git a/app/Models/Role.php b/app/Models/Role.php index c560cd6e3c..5f1a7c33e9 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -16,28 +16,24 @@ namespace FireflyIII\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; - /** * Class Role * * @package FireflyIII\Models - * @property integer $id - * @property string $name - * @property string $display_name - * @property string $description - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\User[] $users - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Role whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Role whereName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Role whereDisplayName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Role whereDescription($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Role whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Role whereUpdatedAt($value) - * @mixin \Eloquent */ class Role extends Model { + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + ]; + protected $dates = ['created_at', 'updated_at']; /** * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany diff --git a/app/Models/Rule.php b/app/Models/Rule.php index 43d3631773..ad3db4309c 100644 --- a/app/Models/Rule.php +++ b/app/Models/Rule.php @@ -21,38 +21,28 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * Class Rule * * @package FireflyIII\Models - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property string $deleted_at - * @property integer $user_id - * @property integer $rule_group_id - * @property integer $order - * @property string $title - * @property string $description - * @property boolean $active - * @property boolean $stop_processing - * @property-read \FireflyIII\User $user - * @property-read \FireflyIII\Models\RuleGroup $ruleGroup - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\RuleAction[] $ruleActions - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\RuleTrigger[] $ruleTriggers - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Rule whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Rule whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Rule whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Rule whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Rule whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Rule whereRuleGroupId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Rule whereOrder($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Rule whereActive($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Rule whereStopProcessing($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Rule whereTitle($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Rule whereDescription($value) - * @mixin \Eloquent */ class Rule extends Model { use SoftDeletes; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'active' => 'boolean', + 'order' => 'int', + 'stop_processing' => 'boolean', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at', 'deleted_at']; + /** * @param Rule $value * diff --git a/app/Models/RuleAction.php b/app/Models/RuleAction.php index 2e13c89b9f..8817a108dc 100644 --- a/app/Models/RuleAction.php +++ b/app/Models/RuleAction.php @@ -10,45 +10,34 @@ */ declare(strict_types = 1); -/** - * RuleAction.php - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ namespace FireflyIII\Models; use Illuminate\Database\Eloquent\Model; - /** - * FireflyIII\Models\RuleAction + * Class RuleAction * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $rule_id - * @property integer $order - * @property boolean $active - * @property boolean $stop_processing - * @property string $action_type - * @property string $action_value - * @property-read \FireflyIII\Models\Rule $rule - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleAction whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleAction whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleAction whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleAction whereRuleId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleAction whereOrder($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleAction whereActive($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleAction whereStopProcessing($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleAction whereActionType($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleAction whereActionValue($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class RuleAction extends Model { + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'active' => 'boolean', + 'order' => 'int', + 'stop_processing' => 'boolean', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at']; + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ diff --git a/app/Models/RuleGroup.php b/app/Models/RuleGroup.php index 532ca0eba2..98cbdc0889 100644 --- a/app/Models/RuleGroup.php +++ b/app/Models/RuleGroup.php @@ -21,31 +21,26 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * Class RuleGroup * * @package FireflyIII\Models - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property string $deleted_at - * @property integer $user_id - * @property integer $order - * @property string $title - * @property string $description - * @property boolean $active - * @property-read \FireflyIII\User $user - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Rule[] $rules - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleGroup whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleGroup whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleGroup whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleGroup whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleGroup whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleGroup whereOrder($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleGroup whereTitle($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleGroup whereDescription($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleGroup whereActive($value) - * @mixin \Eloquent */ class RuleGroup extends Model { use SoftDeletes; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'active' => 'boolean', + 'order' => 'int', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at', 'deleted_at']; + protected $fillable = ['user_id', 'order', 'title', 'description', 'active']; diff --git a/app/Models/RuleTrigger.php b/app/Models/RuleTrigger.php index 684d9a9509..048d1c94fc 100644 --- a/app/Models/RuleTrigger.php +++ b/app/Models/RuleTrigger.php @@ -16,32 +16,28 @@ namespace FireflyIII\Models; use Illuminate\Database\Eloquent\Model; /** - * FireflyIII\Models\RuleTrigger + * Class RuleTrigger * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $rule_id - * @property integer $order - * @property string $title - * @property string $trigger_type - * @property string $trigger_value - * @property boolean $active - * @property boolean $stop_processing - * @property-read \FireflyIII\Models\Rule $rule - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleTrigger whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleTrigger whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleTrigger whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleTrigger whereRuleId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleTrigger whereOrder($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleTrigger whereActive($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleTrigger whereStopProcessing($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleTrigger whereTriggerType($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\RuleTrigger whereTriggerValue($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class RuleTrigger extends Model { + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'active' => 'boolean', + 'order' => 'int', + 'stop_processing' => 'boolean', + ]; + /** @var array */ + protected $dates = ['created_at', 'updated_at']; + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ diff --git a/app/Models/Tag.php b/app/Models/Tag.php index a89220efea..769ea2c5af 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -15,49 +15,36 @@ namespace FireflyIII\Models; use Crypt; use FireflyIII\Support\Models\TagSupport; +use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Watson\Validating\ValidatingTrait; /** - * FireflyIII\Models\Tag + * Class Tag * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property string $deleted_at - * @property integer $user_id - * @property string $tag - * @property string $tagMode - * @property \Carbon\Carbon $date - * @property string $description - * @property float $latitude - * @property float $longitude - * @property integer $zoomLevel - * @property-read \Illuminate\Database\Eloquent\Collection|TransactionJournal[] $transactionjournals - * @property-read \FireflyIII\User $user - * @property int $account_id - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereTag($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereTagMode($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereDate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereDescription($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereLatitude($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereLongitude($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Tag whereZoomLevel($value) - * @mixin \Eloquent - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournal[] $transactionJournals + * @package FireflyIII\Models */ class Tag extends TagSupport { - protected $dates = ['created_at', 'updated_at', 'date']; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'date' => 'date', + 'zoomLevel' => 'int', + + ]; + protected $dates = ['created_at', 'updated_at', 'date', 'deleted_at']; protected $fillable = ['user_id', 'tag', 'date', 'description', 'longitude', 'latitude', 'zoomLevel', 'tagMode']; protected $rules = ['tag' => 'required|between:1,200',]; - use ValidatingTrait; + use ValidatingTrait, SoftDeletes; /** * @param array $fields @@ -71,7 +58,7 @@ class Tag extends TagSupport $search = $fields; unset($search['tag']); - $query = Tag::orderBy('id'); + $query = self::orderBy('id'); foreach ($search as $name => $value) { $query->where($name, $value); } @@ -85,7 +72,7 @@ class Tag extends TagSupport // create it! $fields['tagMode'] = 'nothing'; $fields['description'] = $fields['description'] ?? ''; - $tag = Tag::create($fields); + $tag = self::create($fields); return $tag; diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index f35efcb180..70061c8072 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -20,40 +20,27 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Watson\Validating\ValidatingTrait; /** - * FireflyIII\Models\Transaction + * Class Transaction * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property \Carbon\Carbon $deleted_at - * @property integer $account_id - * @property integer $transaction_journal_id - * @property string $description - * @property float $amount - * @property-read Account $account - * @property-read TransactionJournal $transactionJournal - * @method static \Illuminate\Database\Query\Builder|Transaction after($date) - * @method static \Illuminate\Database\Query\Builder|Transaction before($date) - * @property float $before - * @property float $after - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Transaction whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Transaction whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Transaction whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Transaction whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Transaction whereAccountId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Transaction whereTransactionJournalId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Transaction whereDescription($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Transaction whereAmount($value) - * @mixin \Eloquent - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Category[] $categories - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Transaction transactionTypes($types) - * @property integer $identifier - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\Transaction whereIdentifier($value) + * @package FireflyIII\Models */ 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', + 'identifier' => 'int', + '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']; @@ -64,7 +51,6 @@ class Transaction extends Model 'description' => 'between:0,1024', 'amount' => 'required|numeric', ]; - use SoftDeletes, ValidatingTrait; /** @@ -73,7 +59,7 @@ class Transaction extends Model * * @return bool */ - public static function isJoined(Builder $query, string $table):bool + public static function isJoined(Builder $query, string $table): bool { $joins = $query->getQuery()->joins; if (is_null($joins)) { @@ -171,7 +157,7 @@ class Transaction extends Model */ public function setAmountAttribute($value) { - $this->attributes['amount'] = strval(round($value, 2)); + $this->attributes['amount'] = strval(round($value, 12)); } /** diff --git a/app/Models/TransactionCurrency.php b/app/Models/TransactionCurrency.php index 1cf89fc7de..0a63b24329 100644 --- a/app/Models/TransactionCurrency.php +++ b/app/Models/TransactionCurrency.php @@ -19,33 +19,35 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Watson\Validating\ValidatingTrait; /** - * FireflyIII\Models\TransactionCurrency + * Class TransactionCurrency * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property \Carbon\Carbon $deleted_at - * @property string $code - * @property string $name - * @property string $symbol - * @property-read \Illuminate\Database\Eloquent\Collection|TransactionJournal[] $transactionJournals - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionCurrency whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionCurrency whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionCurrency whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionCurrency whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionCurrency whereCode($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionCurrency whereName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionCurrency whereSymbol($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class TransactionCurrency extends Model { use SoftDeletes, ValidatingTrait; - - protected $dates = ['created_at', 'updated_at', 'deleted_at']; - protected $fillable = ['name', 'code', 'symbol']; - protected $rules = ['name' => 'required|between:1,200', 'code' => 'required|between:3,3', 'symbol' => 'required|between:1,12']; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'decimal_places' => 'int', + ]; + protected $dates = ['created_at', 'updated_at', 'deleted_at', 'date']; + protected $fillable = ['name', 'code', 'symbol', 'decimal_places']; + protected $rules + = [ + 'name' => 'required|between:1,48', + 'code' => 'required|between:3,3', + 'symbol' => 'required|between:1,8', + 'decimal_places' => 'required|min:0|max:12|numeric', + ]; /** * @param TransactionCurrency $currency diff --git a/app/Models/TransactionGroup.php b/app/Models/TransactionGroup.php deleted file mode 100644 index e075cfb63b..0000000000 --- a/app/Models/TransactionGroup.php +++ /dev/null @@ -1,61 +0,0 @@ -belongsToMany('FireflyIII\Models\TransactionJournal'); - } - - /** - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function user() - { - return $this->belongsTo('FireflyIII\User'); - } - -} diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index f6f17b9429..1808955271 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -26,80 +26,33 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Watson\Validating\ValidatingTrait; /** - * FireflyIII\Models\TransactionJournal + * Class TransactionJournal * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property \Carbon\Carbon $deleted_at - * @property integer $user_id - * @property integer $transaction_type_id - * @property integer $bill_id - * @property integer $transaction_currency_id - * @property string $description - * @property boolean $completed - * @property \Carbon\Carbon $date - * @property \Carbon\Carbon $interest_date - * @property \Carbon\Carbon $book_date - * @property boolean $encrypted - * @property integer $order - * @property integer $tag_count - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Attachment[] $attachments - * @property-read \FireflyIII\Models\Bill $bill - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\PiggyBankEvent[] $piggyBankEvents - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Tag[] $tags - * @property-read \FireflyIII\Models\TransactionCurrency $transactionCurrency - * @property-read \FireflyIII\Models\TransactionType $transactionType - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionGroup[] $transactiongroups - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournalMeta[] $transactionjournalmeta - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Transaction[] $transactions - * @property-read \FireflyIII\User $user - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal after($date) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal before($date) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal transactionTypes($types) - * @property string $transaction_type_type - * @property-read string $transaction_currency_code - * @property-read string $destination_amount - * @property string $destination_account_id - * @property string $destination_account_name - * @property-read string $destination_account_type - * @property-read string $source_amount - * @property string $source_account_id - * @property string $source_account_name - * @property-read string $source_account_type - * @property \Carbon\Carbon $process_date - * @property int $account_id - * @property float $journalAmount - * @property string $account_name - * @property int $budget_id - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereTransactionTypeId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereBillId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereTransactionCurrencyId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereDescription($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereCompleted($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereDate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereInterestDate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereBookDate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereProcessDate($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereEncrypted($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereOrder($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal whereTagCount($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal expanded() - * @mixin \Eloquent - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournal sortCorrectly() - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournalMeta[] $transactionJournalMeta + * @package FireflyIII\Models */ class TransactionJournal extends TransactionJournalSupport { use SoftDeletes, ValidatingTrait; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + 'date' => 'date', + 'interest_date' => 'date', + 'book_date' => 'date', + 'process_date' => 'date', + 'order' => 'int', + 'tag_count' => 'int', + 'encrypted' => 'boolean', + 'completed' => 'boolean', + ]; /** @var array */ protected $dates = ['created_at', 'updated_at', 'date', 'deleted_at', 'interest_date', 'book_date', 'process_date']; /** @var array */ @@ -130,12 +83,11 @@ class TransactionJournal extends TransactionJournalSupport public static function routeBinder($value) { if (auth()->check()) { - $validTypes = [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER]; - $object = TransactionJournal::where('transaction_journals.id', $value) - ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->whereIn('transaction_types.type', $validTypes) - ->where('user_id', auth()->user()->id)->first(['transaction_journals.*']); - if ($object) { + $object = self::where('transaction_journals.id', $value) + ->with('transactionType') + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->where('user_id', auth()->user()->id)->first(['transaction_journals.*']); + if (!is_null($object)) { return $object; } } @@ -180,7 +132,7 @@ class TransactionJournal extends TransactionJournalSupport * * @return bool */ - public function deleteMeta(string $name):bool + public function deleteMeta(string $name): bool { $this->transactionJournalMeta()->where('name', $name)->delete(); @@ -408,7 +360,9 @@ class TransactionJournal extends TransactionJournalSupport if (!self::isJoined($query, 'transaction_types')) { $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); } - $query->whereIn('transaction_types.type', $types); + if (count($types) > 0) { + $query->whereIn('transaction_types.type', $types); + } } /** @@ -417,8 +371,9 @@ class TransactionJournal extends TransactionJournalSupport */ public function setDescriptionAttribute($value) { - $this->attributes['description'] = Crypt::encrypt($value); - $this->attributes['encrypted'] = true; + $encrypt = config('firefly.encryption'); + $this->attributes['description'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['encrypted'] = $encrypt; } /** diff --git a/app/Models/TransactionJournalMeta.php b/app/Models/TransactionJournalMeta.php index d855897713..a3f6ec92ba 100644 --- a/app/Models/TransactionJournalMeta.php +++ b/app/Models/TransactionJournalMeta.php @@ -15,33 +15,29 @@ namespace FireflyIII\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\SoftDeletes; /** * Class TransactionJournalMeta * * @package FireflyIII\Models - * @property-read \FireflyIII\Models\TransactionJournal $transactionjournal - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $transaction_journal_id - * @property string $name - * @property string $data - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournalMeta whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournalMeta whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournalMeta whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournalMeta whereTransactionJournalId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournalMeta whereName($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournalMeta whereData($value) - * @mixin \Eloquent - * @property string $hash - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionJournalMeta whereHash($value) - * @property-read \FireflyIII\Models\TransactionJournal $transactionJournal */ class TransactionJournalMeta extends Model { - protected $dates = ['created_at', 'updated_at']; + use SoftDeletes; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + ]; + protected $dates = ['created_at', 'updated_at', 'deleted_at']; protected $fillable = ['transaction_journal_id', 'name', 'data', 'hash']; protected $table = 'journal_meta'; diff --git a/app/Models/TransactionType.php b/app/Models/TransactionType.php index 84a1da7ccf..aceffbd8fb 100644 --- a/app/Models/TransactionType.php +++ b/app/Models/TransactionType.php @@ -15,22 +15,12 @@ namespace FireflyIII\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** - * FireflyIII\Models\TransactionType + * Class TransactionType * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property \Carbon\Carbon $deleted_at - * @property string $type - * @property-read \Illuminate\Database\Eloquent\Collection|TransactionJournal[] $transactionJournals - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionType whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionType whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionType whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionType whereDeletedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\Models\TransactionType whereType($value) - * @mixin \Eloquent + * @package FireflyIII\Models */ class TransactionType extends Model { @@ -41,14 +31,45 @@ class TransactionType extends Model const TRANSFER = 'Transfer'; const OPENING_BALANCE = 'Opening balance'; + /** + * The attributes that should be casted to native types. + * + * @var array + */ + protected $casts + = [ + 'created_at' => 'date', + 'updated_at' => 'date', + 'deleted_at' => 'date', + ]; + protected $dates = ['created_at', 'updated_at', 'deleted_at']; + /** + * @param string $type + * + * @return Model|null|static + */ + public static function routeBinder(string $type) + { + if (!auth()->check()) { + throw new NotFoundHttpException; + } + $transactionType = self::where('type', ucfirst($type))->first(); + if (!is_null($transactionType)) { + return $transactionType; + } + throw new NotFoundHttpException; + + } + + /** * @return bool */ public function isDeposit() { - return $this->type === TransactionType::DEPOSIT; + return $this->type === self::DEPOSIT; } /** @@ -56,7 +77,7 @@ class TransactionType extends Model */ public function isOpeningBalance() { - return $this->type === TransactionType::OPENING_BALANCE; + return $this->type === self::OPENING_BALANCE; } /** @@ -64,7 +85,7 @@ class TransactionType extends Model */ public function isTransfer() { - return $this->type === TransactionType::TRANSFER; + return $this->type === self::TRANSFER; } /** @@ -72,7 +93,7 @@ class TransactionType extends Model */ public function isWithdrawal() { - return $this->type === TransactionType::WITHDRAWAL; + return $this->type === self::WITHDRAWAL; } /** diff --git a/app/Providers/AccountServiceProvider.php b/app/Providers/AccountServiceProvider.php index 0ce5a90329..3035731adb 100644 --- a/app/Providers/AccountServiceProvider.php +++ b/app/Providers/AccountServiceProvider.php @@ -14,7 +14,10 @@ declare(strict_types = 1); namespace FireflyIII\Providers; -use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Repositories\Account\AccountRepository; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Account\AccountTasker; +use FireflyIII\Repositories\Account\AccountTaskerInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -44,8 +47,6 @@ class AccountServiceProvider extends ServiceProvider { $this->registerRepository(); $this->registerTasker(); - - } /** @@ -54,16 +55,16 @@ class AccountServiceProvider extends ServiceProvider private function registerRepository() { $this->app->bind( - 'FireflyIII\Repositories\Account\AccountRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\Account\AccountRepository', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + AccountRepositoryInterface::class, + function (Application $app) { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepository::class); + + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - return app('FireflyIII\Repositories\Account\AccountRepository', $arguments); + return $repository; } ); } @@ -74,16 +75,16 @@ class AccountServiceProvider extends ServiceProvider private function registerTasker() { $this->app->bind( - 'FireflyIII\Repositories\Account\AccountTaskerInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\Account\AccountTasker', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + AccountTaskerInterface::class, + function (Application $app) { + /** @var AccountTaskerInterface $tasker */ + $tasker = app(AccountTasker::class); + + if ($app->auth->check()) { + $tasker->setUser(auth()->user()); } - return app('FireflyIII\Repositories\Account\AccountTasker', $arguments); + return $tasker; } ); } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php old mode 100755 new mode 100644 index 56ff3467cf..d8dd3843f5 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -33,7 +33,7 @@ class AppServiceProvider extends ServiceProvider // force root URL. $forcedUrl = env('APP_FORCE_ROOT', ''); - if (strlen($forcedUrl) > 0) { + if (strlen(strval($forcedUrl)) > 0) { URL::forceRootUrl($forcedUrl); } diff --git a/app/Providers/AttachmentServiceProvider.php b/app/Providers/AttachmentServiceProvider.php index 77debb7354..c9f3a9d48d 100644 --- a/app/Providers/AttachmentServiceProvider.php +++ b/app/Providers/AttachmentServiceProvider.php @@ -14,7 +14,8 @@ declare(strict_types = 1); namespace FireflyIII\Providers; -use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Repositories\Attachment\AttachmentRepository; +use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -43,16 +44,15 @@ class AttachmentServiceProvider extends ServiceProvider public function register() { $this->app->bind( - 'FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\Attachment\AttachmentRepository', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + AttachmentRepositoryInterface::class, + function (Application $app) { + /** @var AttachmentRepositoryInterface $repository */ + $repository = app(AttachmentRepository::class); + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - return app('FireflyIII\Repositories\Attachment\AttachmentRepository', $arguments); + return $repository; } ); } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php old mode 100755 new mode 100644 diff --git a/app/Providers/BillServiceProvider.php b/app/Providers/BillServiceProvider.php index 7275348a8d..a07e8e37ff 100644 --- a/app/Providers/BillServiceProvider.php +++ b/app/Providers/BillServiceProvider.php @@ -14,7 +14,8 @@ declare(strict_types = 1); namespace FireflyIII\Providers; -use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Repositories\Bill\BillRepository; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -43,16 +44,16 @@ class BillServiceProvider extends ServiceProvider public function register() { $this->app->bind( - 'FireflyIII\Repositories\Bill\BillRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\Bill\BillRepository', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + BillRepositoryInterface::class, + function (Application $app) { + /** @var BillRepositoryInterface $repository */ + $repository = app(BillRepository::class); + + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - return app('FireflyIII\Repositories\Bill\BillRepository', $arguments); + return $repository; } ); } diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php old mode 100755 new mode 100644 diff --git a/app/Providers/BudgetServiceProvider.php b/app/Providers/BudgetServiceProvider.php index 51408c5098..df87739ea6 100644 --- a/app/Providers/BudgetServiceProvider.php +++ b/app/Providers/BudgetServiceProvider.php @@ -14,7 +14,8 @@ declare(strict_types = 1); namespace FireflyIII\Providers; -use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Repositories\Budget\BudgetRepository; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -43,16 +44,15 @@ class BudgetServiceProvider extends ServiceProvider public function register() { $this->app->bind( - 'FireflyIII\Repositories\Budget\BudgetRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\Budget\BudgetRepository', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + BudgetRepositoryInterface::class, + function (Application $app) { + /** @var BudgetRepositoryInterface $repository */ + $repository = app(BudgetRepository::class); + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - return app('FireflyIII\Repositories\Budget\BudgetRepository', $arguments); + return $repository; } ); } diff --git a/app/Providers/CategoryServiceProvider.php b/app/Providers/CategoryServiceProvider.php index a3deb55f30..bdac9c91f1 100644 --- a/app/Providers/CategoryServiceProvider.php +++ b/app/Providers/CategoryServiceProvider.php @@ -14,7 +14,8 @@ declare(strict_types = 1); namespace FireflyIII\Providers; -use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Repositories\Category\CategoryRepository; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -43,16 +44,15 @@ class CategoryServiceProvider extends ServiceProvider public function register() { $this->app->bind( - 'FireflyIII\Repositories\Category\CategoryRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\Category\CategoryRepository', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + CategoryRepositoryInterface::class, + function (Application $app) { + /** @var CategoryRepository $repository */ + $repository = app(CategoryRepository::class); + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - return app('FireflyIII\Repositories\Category\CategoryRepository', $arguments); + return $repository; } ); diff --git a/app/Providers/CrudServiceProvider.php b/app/Providers/CurrencyServiceProvider.php similarity index 51% rename from app/Providers/CrudServiceProvider.php rename to app/Providers/CurrencyServiceProvider.php index 93acfa1412..bfdf535c70 100644 --- a/app/Providers/CrudServiceProvider.php +++ b/app/Providers/CurrencyServiceProvider.php @@ -1,6 +1,6 @@ registerJournal(); - } - - private function registerJournal() { $this->app->bind( - 'FireflyIII\Crud\Split\JournalInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Crud\Split\Journal', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + CurrencyRepositoryInterface::class, + function (Application $app) { + /** @var CurrencyRepository $repository */ + $repository = app(CurrencyRepository::class); + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - return app('FireflyIII\Crud\Split\Journal', $arguments); + return $repository; } ); - } + } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php old mode 100755 new mode 100644 index 6cfb886b4c..215fc5978e --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -18,6 +18,7 @@ use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBankRepetition; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionJournalMeta; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Log; @@ -36,31 +37,14 @@ class EventServiceProvider extends ServiceProvider protected $listen = [ // new event handlers: - 'FireflyIII\Events\ConfirmedUser' => // is a User related event. - [ - 'FireflyIII\Handlers\Events\UserEventHandler@storeConfirmationIpAddress', - ], - 'FireflyIII\Events\RegisteredUser' => // is a User related event. + 'FireflyIII\Events\RegisteredUser' => // is a User related event. [ 'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail', 'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole', - 'FireflyIII\Handlers\Events\UserEventHandler@sendConfirmationMessage', - 'FireflyIII\Handlers\Events\UserEventHandler@storeRegistrationIpAddress', ], - 'FireflyIII\Events\ResentConfirmation' => // is a User related event. - [ - 'FireflyIII\Handlers\Events\UserEventHandler@sendConfirmationMessageAgain', - ], - 'FireflyIII\Events\StoredBudgetLimit' => // is a Budget related event. - [ - 'FireflyIII\Handlers\Events\BudgetEventHandler@storeRepetition', - ], - - 'FireflyIII\Events\UpdatedBudgetLimit' => // is a Budget related event. - [ - 'FireflyIII\Handlers\Events\BudgetEventHandler@updateRepetition', - ], - + 'FireflyIII\Events\RequestedNewPassword' => [ // is a User related event. + 'FireflyIII\Handlers\Events\UserEventHandler@sendNewPassword', + ], 'FireflyIII\Events\StoredTransactionJournal' => // is a Transaction Journal related event. [ 'FireflyIII\Handlers\Events\StoredJournalEventHandler@scanBills', @@ -70,7 +54,6 @@ class EventServiceProvider extends ServiceProvider 'FireflyIII\Events\UpdatedTransactionJournal' => // is a Transaction Journal related event. [ 'FireflyIII\Handlers\Events\UpdatedJournalEventHandler@scanBills', - 'FireflyIII\Handlers\Events\UpdatedJournalEventHandler@connectToPiggyBank', 'FireflyIII\Handlers\Events\UpdatedJournalEventHandler@processRules', ], @@ -135,13 +118,21 @@ class EventServiceProvider extends ServiceProvider TransactionJournal::deleted( function (TransactionJournal $journal) { - Log::debug('Now triggered journal delete response #' . $journal->id); + Log::debug(sprintf('Now triggered journal delete response #%d', $journal->id)); /** @var Transaction $transaction */ foreach ($journal->transactions()->get() as $transaction) { - Log::debug('Will now delete transaction #' . $transaction->id); + Log::debug(sprintf('Will now delete transaction #%d', $transaction->id)); $transaction->delete(); } + + // also delete journal_meta entries. + + /** @var TransactionJournalMeta $meta */ + foreach ($journal->transactionJournalMeta()->get() as $meta) { + Log::debug(sprintf('Will now delete meta-entry #%d', $meta->id)); + $meta->delete(); + } } ); diff --git a/app/Providers/ExportJobServiceProvider.php b/app/Providers/ExportJobServiceProvider.php index 220a678623..6850f417b5 100644 --- a/app/Providers/ExportJobServiceProvider.php +++ b/app/Providers/ExportJobServiceProvider.php @@ -14,7 +14,10 @@ declare(strict_types = 1); namespace FireflyIII\Providers; -use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Repositories\ExportJob\ExportJobRepository; +use FireflyIII\Repositories\ExportJob\ExportJobRepositoryInterface; +use FireflyIII\Repositories\ImportJob\ImportJobRepository; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -33,8 +36,7 @@ class ExportJobServiceProvider extends ServiceProvider */ public function boot() { - $this->exportJob(); - $this->importJob(); + } @@ -45,7 +47,8 @@ class ExportJobServiceProvider extends ServiceProvider */ public function register() { - // + $this->exportJob(); + $this->importJob(); } /** @@ -53,18 +56,16 @@ class ExportJobServiceProvider extends ServiceProvider */ private function exportJob() { - $this->app->bind( - 'FireflyIII\Repositories\ExportJob\ExportJobRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\ExportJob\ExportJobRepository', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + ExportJobRepositoryInterface::class, + function (Application $app) { + /** @var ExportJobRepository $repository */ + $repository = app(ExportJobRepository::class); + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - return app('FireflyIII\Repositories\ExportJob\ExportJobRepository', $arguments); + return $repository; } ); } @@ -72,16 +73,15 @@ class ExportJobServiceProvider extends ServiceProvider private function importJob() { $this->app->bind( - 'FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\ImportJob\ImportJobRepository', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + ImportJobRepositoryInterface::class, + function (Application $app) { + /** @var ImportJobRepository $repository */ + $repository = app(ImportJobRepository::class); + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - return app('FireflyIII\Repositories\ImportJob\ImportJobRepository', $arguments); + return $repository; } ); } diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index b1530b08ca..2ef97e48d2 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -13,6 +13,28 @@ declare(strict_types = 1); namespace FireflyIII\Providers; +use FireflyIII\Export\Processor; +use FireflyIII\Export\ProcessorInterface; +use FireflyIII\Generator\Chart\Basic\ChartJsGenerator; +use FireflyIII\Generator\Chart\Basic\GeneratorInterface; +use FireflyIII\Helpers\Attachments\AttachmentHelper; +use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; +use FireflyIII\Helpers\Chart\MetaPieChart; +use FireflyIII\Helpers\Chart\MetaPieChartInterface; +use FireflyIII\Helpers\FiscalHelper; +use FireflyIII\Helpers\FiscalHelperInterface; +use FireflyIII\Helpers\Help\Help; +use FireflyIII\Helpers\Help\HelpInterface; +use FireflyIII\Helpers\Report\BalanceReportHelper; +use FireflyIII\Helpers\Report\BalanceReportHelperInterface; +use FireflyIII\Helpers\Report\BudgetReportHelper; +use FireflyIII\Helpers\Report\BudgetReportHelperInterface; +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; use FireflyIII\Support\ExpandedForm; use FireflyIII\Support\FireflyConfig; @@ -93,28 +115,23 @@ class FireflyServiceProvider extends ServiceProvider } ); - $this->app->bind('FireflyIII\Repositories\Currency\CurrencyRepositoryInterface', 'FireflyIII\Repositories\Currency\CurrencyRepository'); - $this->app->bind('FireflyIII\Support\Search\SearchInterface', 'FireflyIII\Support\Search\Search'); - $this->app->bind('FireflyIII\Repositories\User\UserRepositoryInterface', 'FireflyIII\Repositories\User\UserRepository'); - $this->app->bind('FireflyIII\Helpers\Attachments\AttachmentHelperInterface', 'FireflyIII\Helpers\Attachments\AttachmentHelper'); - $this->app->bind( - 'FireflyIII\Generator\Chart\Account\AccountChartGeneratorInterface', 'FireflyIII\Generator\Chart\Account\ChartJsAccountChartGenerator' - ); - $this->app->bind('FireflyIII\Generator\Chart\Bill\BillChartGeneratorInterface', 'FireflyIII\Generator\Chart\Bill\ChartJsBillChartGenerator'); - $this->app->bind('FireflyIII\Generator\Chart\Budget\BudgetChartGeneratorInterface', 'FireflyIII\Generator\Chart\Budget\ChartJsBudgetChartGenerator'); - $this->app->bind( - 'FireflyIII\Generator\Chart\Category\CategoryChartGeneratorInterface', 'FireflyIII\Generator\Chart\Category\ChartJsCategoryChartGenerator' - ); - $this->app->bind( - 'FireflyIII\Generator\Chart\PiggyBank\PiggyBankChartGeneratorInterface', 'FireflyIII\Generator\Chart\PiggyBank\ChartJsPiggyBankChartGenerator' - ); - $this->app->bind('FireflyIII\Generator\Chart\Report\ReportChartGeneratorInterface', 'FireflyIII\Generator\Chart\Report\ChartJsReportChartGenerator'); - $this->app->bind('FireflyIII\Helpers\Help\HelpInterface', 'FireflyIII\Helpers\Help\Help'); - $this->app->bind('FireflyIII\Helpers\Report\ReportHelperInterface', 'FireflyIII\Helpers\Report\ReportHelper'); - $this->app->bind('FireflyIII\Helpers\FiscalHelperInterface', 'FireflyIII\Helpers\FiscalHelper'); - $this->app->bind('FireflyIII\Helpers\Report\AccountReportHelperInterface', 'FireflyIII\Helpers\Report\AccountReportHelper'); - $this->app->bind('FireflyIII\Helpers\Report\BalanceReportHelperInterface', 'FireflyIII\Helpers\Report\BalanceReportHelper'); - $this->app->bind('FireflyIII\Helpers\Report\BudgetReportHelperInterface', 'FireflyIII\Helpers\Report\BudgetReportHelper'); + // chart generator: + $this->app->bind(GeneratorInterface::class, ChartJsGenerator::class); + + // chart builder + $this->app->bind(MetaPieChartInterface::class, MetaPieChart::class); + + // 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); + + $this->app->bind(HelpInterface::class, Help::class); + $this->app->bind(ReportHelperInterface::class, ReportHelper::class); + $this->app->bind(FiscalHelperInterface::class,FiscalHelper::class); + $this->app->bind(BalanceReportHelperInterface::class, BalanceReportHelper::class); + $this->app->bind(BudgetReportHelperInterface::class, BudgetReportHelper::class); } } diff --git a/app/Providers/FireflySessionProvider.php b/app/Providers/FireflySessionProvider.php new file mode 100644 index 0000000000..47dfb1bfe7 --- /dev/null +++ b/app/Providers/FireflySessionProvider.php @@ -0,0 +1,64 @@ +registerSessionManager(); + + $this->registerSessionDriver(); + + $this->app->singleton(StartFireflySession::class); + } + + /** + * Register the session driver instance. + * + * @return void + */ + protected function registerSessionDriver() + { + $this->app->singleton( + 'session.store', function ($app) { + // First, we will create the session manager which is responsible for the + // creation of the various session drivers when they are needed by the + // application instance, and will resolve them on a lazy load basis. + return $app->make('session')->driver(); + } + ); + } + + /** + * Register the session manager instance. + * + * @return void + */ + protected function registerSessionManager() + { + $this->app->singleton( + 'session', function ($app) { + return new SessionManager($app); + } + ); + } +} diff --git a/app/Providers/JournalServiceProvider.php b/app/Providers/JournalServiceProvider.php index 5353941000..b44c11b760 100644 --- a/app/Providers/JournalServiceProvider.php +++ b/app/Providers/JournalServiceProvider.php @@ -14,7 +14,12 @@ declare(strict_types = 1); namespace FireflyIII\Providers; -use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\JournalCollector; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Repositories\Journal\JournalRepository; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalTasker; +use FireflyIII\Repositories\Journal\JournalTaskerInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -44,21 +49,39 @@ class JournalServiceProvider extends ServiceProvider { $this->registerRepository(); $this->registerTasker(); + $this->registerCollector(); + } + + private function registerCollector() + { + $this->app->bind( + JournalCollectorInterface::class, + function (Application $app) { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollector::class); + if ($app->auth->check()) { + $collector->setUser(auth()->user()); + } + $collector->startQuery(); + + return $collector; + } + ); } private function registerRepository() { $this->app->bind( - 'FireflyIII\Repositories\Journal\JournalRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\Journal\JournalRepository', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + JournalRepositoryInterface::class, + function (Application $app) { + /** @var JournalRepositoryInterface $repository */ + $repository = app(JournalRepository::class); + if ($app->auth->check()) { + + $repository->setUser(auth()->user()); } - return app('FireflyIII\Repositories\Journal\JournalRepository', $arguments); + return $repository; } ); } @@ -66,16 +89,16 @@ class JournalServiceProvider extends ServiceProvider private function registerTasker() { $this->app->bind( - 'FireflyIII\Repositories\Journal\JournalTaskerInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\Journal\JournalTasker', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + JournalTaskerInterface::class, + function (Application $app) { + /** @var JournalTaskerInterface $tasker */ + $tasker = app(JournalTasker::class); + + if ($app->auth->check()) { + $tasker->setUser(auth()->user()); } - return app('FireflyIII\Repositories\Journal\JournalTasker', $arguments); + return $tasker; } ); } diff --git a/app/Providers/LogServiceProvider.php b/app/Providers/LogServiceProvider.php new file mode 100644 index 0000000000..dc742d6b80 --- /dev/null +++ b/app/Providers/LogServiceProvider.php @@ -0,0 +1,53 @@ +useDailyFiles( + $this->app->storagePath() . '/logs/firefly-iii.log', $this->maxFiles(), + $this->logLevel() + ); + } + + /** + * Configure the Monolog handlers for the application. + * + * @param \Illuminate\Log\Writer $log + * + * @return void + */ + protected function configureSingleHandler(Writer $log) + { + $log->useFiles( + $this->app->storagePath() . '/logs/firefly-iii.log', + $this->logLevel() + ); + } +} diff --git a/app/Providers/PiggyBankServiceProvider.php b/app/Providers/PiggyBankServiceProvider.php index 11ba540e3c..fb1aaf7d5f 100644 --- a/app/Providers/PiggyBankServiceProvider.php +++ b/app/Providers/PiggyBankServiceProvider.php @@ -15,6 +15,8 @@ declare(strict_types = 1); namespace FireflyIII\Providers; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Repositories\PiggyBank\PiggyBankRepository; +use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -44,16 +46,14 @@ class PiggyBankServiceProvider extends ServiceProvider public function register() { $this->app->bind( - 'FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\PiggyBank\PiggyBankRepository', [auth()->user()]); + PiggyBankRepositoryInterface::class, + function (Application $app) { + /** @var PiggyBankRepository $repository */ + $repository = app(PiggyBankRepository::class); + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); - } - - return app('FireflyIII\Repositories\PiggyBank\PiggyBankRepository', $arguments); + return $repository; } ); } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php old mode 100755 new mode 100644 diff --git a/app/Providers/RuleGroupServiceProvider.php b/app/Providers/RuleGroupServiceProvider.php index bddd6a8480..0a3c8db599 100644 --- a/app/Providers/RuleGroupServiceProvider.php +++ b/app/Providers/RuleGroupServiceProvider.php @@ -14,7 +14,8 @@ declare(strict_types = 1); namespace FireflyIII\Providers; -use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Repositories\RuleGroup\RuleGroupRepository; +use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -44,16 +45,15 @@ class RuleGroupServiceProvider extends ServiceProvider public function register() { $this->app->bind( - 'FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\RuleGroup\RuleGroupRepository', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + RuleGroupRepositoryInterface::class, + function (Application $app) { + /** @var RuleGroupRepository $repository */ + $repository = app(RuleGroupRepository::class); + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - return app('FireflyIII\Repositories\RuleGroup\RuleGroupRepository', $arguments); + return $repository; } ); } diff --git a/app/Providers/RuleServiceProvider.php b/app/Providers/RuleServiceProvider.php index a395e8cda3..5d694411b3 100644 --- a/app/Providers/RuleServiceProvider.php +++ b/app/Providers/RuleServiceProvider.php @@ -14,7 +14,8 @@ declare(strict_types = 1); namespace FireflyIII\Providers; -use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Repositories\Rule\RuleRepository; +use FireflyIII\Repositories\Rule\RuleRepositoryInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -43,16 +44,14 @@ class RuleServiceProvider extends ServiceProvider public function register() { $this->app->bind( - 'FireflyIII\Repositories\Rule\RuleRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\Rule\RuleRepository', [auth()->user()]); + RuleRepositoryInterface::class, + function (Application $app) { + /** @var RuleRepository $repository */ + $repository = app(RuleRepository::class); + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); - } - - return app('FireflyIII\Repositories\Rule\RuleRepository', $arguments); + return $repository; } ); } diff --git a/app/Providers/SearchServiceProvider.php b/app/Providers/SearchServiceProvider.php new file mode 100644 index 0000000000..18d5b48c6e --- /dev/null +++ b/app/Providers/SearchServiceProvider.php @@ -0,0 +1,59 @@ +app->bind( + SearchInterface::class, + function (Application $app) { + /** @var Search $search */ + $search = app(Search::class); + if ($app->auth->check()) { + $search->setUser(auth()->user()); + } + + return $search; + } + ); + } +} diff --git a/app/Providers/TagServiceProvider.php b/app/Providers/TagServiceProvider.php index 8889c9fbeb..b5daa36c6c 100644 --- a/app/Providers/TagServiceProvider.php +++ b/app/Providers/TagServiceProvider.php @@ -14,7 +14,8 @@ declare(strict_types = 1); namespace FireflyIII\Providers; -use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Repositories\Tag\TagRepository; +use FireflyIII\Repositories\Tag\TagRepositoryInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -43,16 +44,16 @@ class TagServiceProvider extends ServiceProvider public function register() { $this->app->bind( - 'FireflyIII\Repositories\Tag\TagRepositoryInterface', - function (Application $app, array $arguments) { - if (!isset($arguments[0]) && $app->auth->check()) { - return app('FireflyIII\Repositories\Tag\TagRepository', [auth()->user()]); - } - if (!isset($arguments[0]) && !$app->auth->check()) { - throw new FireflyException('There is no user present.'); + TagRepositoryInterface::class, + function (Application $app) { + /** @var TagRepository $repository */ + $repository = app(TagRepository::class); + + if ($app->auth->check()) { + $repository->setUser(auth()->user()); } - return app('FireflyIII\Repositories\Tag\TagRepository', $arguments); + return $repository; } ); } diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index b7421c1e6b..26ba26b6ea 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -41,17 +41,7 @@ class AccountRepository implements AccountRepositoryInterface /** @var User */ private $user; /** @var array */ - private $validFields = ['accountRole', 'ccMonthlyPaymentDate', 'ccType', 'accountNumber']; - - /** - * AttachmentRepository constructor. - * - * @param User $user - */ - public function __construct(User $user) - { - $this->user = $user; - } + private $validFields = ['accountRole', 'ccMonthlyPaymentDate', 'ccType', 'accountNumber', 'currency_id', 'BIC']; /** * Moved here from account CRUD @@ -60,7 +50,7 @@ class AccountRepository implements AccountRepositoryInterface * * @return int */ - public function count(array $types):int + public function count(array $types): int { $count = $this->user->accounts()->accountTypeIn($types)->count(); @@ -260,6 +250,52 @@ class AccountRepository implements AccountRepositoryInterface return $result; } + /** + * Returns the date of the very last transaction in this account. + * + * @param Account $account + * + * @return Carbon + */ + public function newestJournalDate(Account $account): Carbon + { + $last = new Carbon; + $date = $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->orderBy('transaction_journals.date', 'DESC') + ->orderBy('transaction_journals.order', 'ASC') + ->orderBy('transaction_journals.id', 'DESC') + ->first(['transaction_journals.date']); + if (!is_null($date)) { + $last = new Carbon($date->date); + } + + return $last; + } + + /** + * Returns the date of the very first transaction in this account. + * + * @param Account $account + * + * @return TransactionJournal + */ + public function oldestJournal(Account $account): TransactionJournal + { + $first = $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->orderBy('transaction_journals.date', 'ASC') + ->orderBy('transaction_journals.order', 'DESC') + ->where('transaction_journals.user_id', $this->user->id) + ->orderBy('transaction_journals.id', 'ASC') + ->first(['transaction_journals.id']); + if (!is_null($first)) { + return TransactionJournal::find(intval($first->id)); + } + + return new TransactionJournal(); + } + /** * Returns the date of the very first transaction in this account. * @@ -269,20 +305,20 @@ class AccountRepository implements AccountRepositoryInterface */ public function oldestJournalDate(Account $account): Carbon { - $first = new Carbon; - - /** @var Transaction $first */ - $date = $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->orderBy('transaction_journals.date', 'ASC') - ->orderBy('transaction_journals.order', 'DESC') - ->orderBy('transaction_journals.id', 'ASC') - ->first(['transaction_journals.date']); - if (!is_null($date)) { - $first = new Carbon($date->date); + $journal = $this->oldestJournal($account); + if (is_null($journal->id)) { + return new Carbon; } - return $first; + return $journal->date; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; } /** @@ -322,7 +358,9 @@ class AccountRepository implements AccountRepositoryInterface $account->save(); $this->updateMetadata($account, $data); - $this->updateInitialBalance($account, $data); + if ($this->validOpeningBalanceData($data)) { + $this->updateInitialBalance($account, $data); + } return $account; } @@ -346,12 +384,11 @@ class AccountRepository implements AccountRepositoryInterface */ protected function openingBalanceTransaction(Account $account): TransactionJournal { - $journal = TransactionJournal - ::sortCorrectly() - ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transactions.account_id', $account->id) - ->transactionTypes([TransactionType::OPENING_BALANCE]) - ->first(['transaction_journals.*']); + $journal = TransactionJournal::sortCorrectly() + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->transactionTypes([TransactionType::OPENING_BALANCE]) + ->first(['transaction_journals.*']); if (is_null($journal)) { Log::debug('Could not find a opening balance journal, return empty one.'); @@ -418,7 +455,7 @@ class AccountRepository implements AccountRepositoryInterface { $amount = $data['openingBalance']; $name = $data['name']; - $opposing = $this->storeOpposingAccount($amount, $name); + $opposing = $this->storeOpposingAccount($name); $transactionType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first(); $journal = TransactionJournal::create( [ @@ -428,7 +465,6 @@ class AccountRepository implements AccountRepositoryInterface 'description' => 'Initial balance for "' . $account->name . '"', 'completed' => true, 'date' => $data['openingBalanceDate'], - 'encrypted' => true, ] ); Log::debug(sprintf('Created new opening balance journal: #%d', $journal->id)); @@ -456,22 +492,20 @@ class AccountRepository implements AccountRepositoryInterface } /** - * @param float $amount * @param string $name * * @return Account */ - protected function storeOpposingAccount(float $amount, string $name):Account + protected function storeOpposingAccount(string $name): Account { - $type = $amount < 0 ? 'expense' : 'revenue'; $opposingData = [ - 'accountType' => $type, + 'accountType' => 'initial', 'name' => $name . ' initial balance', 'active' => false, 'iban' => '', 'virtualBalance' => 0, ]; - Log::debug('Going to create an opening balance opposing account'); + Log::debug('Going to create an opening balance opposing account.'); return $this->storeAccount($opposingData); } @@ -602,5 +636,4 @@ class AccountRepository implements AccountRepositoryInterface return false; } - } diff --git a/app/Repositories/Account/AccountRepositoryInterface.php b/app/Repositories/Account/AccountRepositoryInterface.php index ab7b1f5dc9..b0ab9b738b 100644 --- a/app/Repositories/Account/AccountRepositoryInterface.php +++ b/app/Repositories/Account/AccountRepositoryInterface.php @@ -15,6 +15,8 @@ namespace FireflyIII\Repositories\Account; use Carbon\Carbon; use FireflyIII\Models\Account; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\User; use Illuminate\Support\Collection; /** @@ -96,6 +98,24 @@ interface AccountRepositoryInterface */ public function getActiveAccountsByType(array $types): Collection; + /** + * Returns the date of the very last transaction in this account. + * + * @param Account $account + * + * @return Carbon + */ + public function newestJournalDate(Account $account): Carbon; + + /** + * Returns the date of the very first transaction in this account. + * + * @param Account $account + * + * @return TransactionJournal + */ + public function oldestJournal(Account $account): TransactionJournal; + /** * Returns the date of the very first transaction in this account. * @@ -105,11 +125,16 @@ interface AccountRepositoryInterface */ public function oldestJournalDate(Account $account): Carbon; + /** + * @param User $user + */ + public function setUser(User $user); + /** * @param array $data * * @return Account */ - public function store(array $data) : Account; + public function store(array $data): Account; } diff --git a/app/Repositories/Account/AccountTasker.php b/app/Repositories/Account/AccountTasker.php index 59b46c1b88..e5c81eb211 100644 --- a/app/Repositories/Account/AccountTasker.php +++ b/app/Repositories/Account/AccountTasker.php @@ -14,17 +14,11 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Account; use Carbon\Carbon; -use Crypt; -use DB; -use FireflyIII\Helpers\Collection\Account as AccountCollection; -use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; -use FireflyIII\Models\TransactionType; use FireflyIII\User; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; use Log; -use stdClass; use Steam; /** @@ -37,16 +31,6 @@ class AccountTasker implements AccountTaskerInterface /** @var User */ private $user; - /** - * AttachmentRepository constructor. - * - * @param User $user - */ - public function __construct(User $user) - { - $this->user = $user; - } - /** * @see self::amountInPeriod * @@ -107,187 +91,68 @@ class AccountTasker implements AccountTaskerInterface /** * @param Collection $accounts - * @param Collection $excluded * @param Carbon $start * @param Carbon $end * - * @return Collection - * @see self::financialReport - * + * @return array */ - public function expenseReport(Collection $accounts, Collection $excluded, Carbon $start, Carbon $end): Collection + public function getAccountReport(Collection $accounts, Carbon $start, Carbon $end): array { - $idList = [ - 'accounts' => $accounts->pluck('id')->toArray(), - 'exclude' => $excluded->pluck('id')->toArray(), - ]; - - Log::debug( - 'Now calling expenseReport.', - ['accounts' => $idList['accounts'], 'excluded' => $idList['exclude'], - 'start' => $start->format('Y-m-d'), - 'end' => $end->format('Y-m-d'), - ] - ); - - return $this->financialReport($idList, $start, $end, false); - - } - - /** - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return AccountCollection - */ - public function getAccountReport(Carbon $start, Carbon $end, Collection $accounts): AccountCollection - { - $startAmount = '0'; - $endAmount = '0'; - $diff = '0'; - $ids = $accounts->pluck('id')->toArray(); - $yesterday = clone $start; + $ids = $accounts->pluck('id')->toArray(); + $yesterday = clone $start; $yesterday->subDay(); - $startSet = Steam::balancesById($ids, $yesterday); - $backupSet = Steam::balancesById($ids, $start); - $endSet = Steam::balancesById($ids, $end); + $startSet = Steam::balancesById($ids, $yesterday); + $endSet = Steam::balancesById($ids, $end); - Log::debug( - sprintf( - 'getAccountReport from %s to %s for %d accounts.', - $start->format('Y-m-d'), - $end->format('Y-m-d'), - $accounts->count() - ) - ); - $accounts->each( - function (Account $account) use ($startSet, $endSet, $backupSet) { - $account->startBalance = $startSet[$account->id] ?? '0'; - $account->endBalance = $endSet[$account->id] ?? '0'; + Log::debug('Start of accountreport'); - // check backup set just in case: - if ($account->startBalance === '0' && isset($backupSet[$account->id])) { - $account->startBalance = $backupSet[$account->id]; - } - } - ); + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); - // summarize: - foreach ($accounts as $account) { - $startAmount = bcadd($startAmount, $account->startBalance); - $endAmount = bcadd($endAmount, $account->endBalance); - $diff = bcadd($diff, bcsub($account->endBalance, $account->startBalance)); - } - - $object = new AccountCollection; - $object->setStart($startAmount); - $object->setEnd($endAmount); - $object->setDifference($diff); - $object->setAccounts($accounts); - - - return $object; - } - - /** - * It might be worth it to expand this query to include all account information required. - * - * @param Collection $accounts - * @param array $types - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function getJournalsInPeriod(Collection $accounts, array $types, Carbon $start, Carbon $end): Collection - { - $accountIds = $accounts->pluck('id')->toArray(); - $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') - ->whereIn('transactions.account_id', $accountIds) - ->whereNull('transactions.deleted_at') - ->whereNull('transaction_journals.deleted_at') - ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) - ->where('transaction_journals.user_id', $this->user->id) - ->orderBy('transaction_journals.date', 'DESC') - ->orderBy('transaction_journals.order', 'ASC') - ->orderBy('transaction_journals.id', 'DESC'); - - if (count($types) > 0) { - $query->whereIn('transaction_types.type', $types); - } - - $set = $query->get( - [ - 'transaction_journals.id as journal_id', - 'transaction_journals.description', - 'transaction_journals.date', - 'transaction_journals.encrypted', - //'transaction_journals.transaction_currency_id', - 'transaction_currencies.code as transaction_currency_code', - //'transaction_currencies.symbol as transaction_currency_symbol', - 'transaction_types.type as transaction_type_type', - 'transaction_journals.bill_id', - 'bills.name as bill_name', - 'transactions.id as id', - 'transactions.amount as transaction_amount', - 'transactions.description as transaction_description', - 'transactions.account_id', - 'transactions.identifier', - 'transactions.transaction_journal_id', - 'accounts.name as account_name', - 'accounts.encrypted as account_encrypted', - 'account_types.type as account_type', - - ] - ); - - // loop for decryption. - $set->each( - function (Transaction $transaction) { - $transaction->date = new Carbon($transaction->date); - $transaction->description = intval($transaction->encrypted) === 1 ? Crypt::decrypt($transaction->description) : $transaction->description; - $transaction->bill_name = !is_null($transaction->bill_name) ? Crypt::decrypt($transaction->bill_name) : ''; - } - ); - - return $set; - } - - /** - * @param Collection $accounts - * @param Collection $excluded - * @param Carbon $start - * @param Carbon $end - * - * @see AccountTasker::financialReport() - * - * @return Collection - * - */ - public function incomeReport(Collection $accounts, Collection $excluded, Carbon $start, Carbon $end): Collection - { - $idList = [ - 'accounts' => $accounts->pluck('id')->toArray(), - 'exclude' => $excluded->pluck('id')->toArray(), + $return = [ + 'start' => '0', + 'end' => '0', + 'difference' => '0', + 'accounts' => [], ]; - Log::debug( - 'Now calling expenseReport.', - ['accounts' => $idList['accounts'], 'excluded' => $idList['exclude'], - 'start' => $start->format('Y-m-d'), - 'end' => $end->format('Y-m-d'), - ] - ); + foreach ($accounts as $account) { + $id = $account->id; + $entry = [ + 'name' => $account->name, + 'id' => $account->id, + 'start_balance' => '0', + 'end_balance' => '0', + ]; - return $this->financialReport($idList, $start, $end, true); + // get first journal date: + $first = $repository->oldestJournal($account); + Log::debug(sprintf('Date of first journal for %s is %s', $account->name, $first->date->format('Y-m-d'))); + $entry['start_balance'] = $startSet[$account->id] ?? '0'; + $entry['end_balance'] = $endSet[$account->id] ?? '0'; + + // first journal exists, and is on start, then this is the actual opening balance: + if (!is_null($first->id) && $first->date->isSameDay($start)) { + $entry['start_balance'] = $first->transactions()->where('account_id', $account->id)->first()->amount; + Log::debug(sprintf('Account %s was opened on %s, so opening balance is %f', $account->name, $start->format('Y-m-d'), $entry['start_balance'])); + } + $return['start'] = bcadd($return['start'], $entry['start_balance']); + $return['end'] = bcadd($return['end'], $entry['end_balance']); + + $return['accounts'][$id] = $entry; + } + + $return['difference'] = bcsub($return['end'], $return['start']); + + return $return; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; } /** @@ -320,22 +185,20 @@ class AccountTasker implements AccountTaskerInterface $joinModifier = $incoming ? '<' : '>'; $selection = $incoming ? '>' : '<'; - $query = Transaction - ::distinct() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->leftJoin( - 'transactions as other_side', function (JoinClause $join) use ($joinModifier) { - $join->on('transaction_journals.id', '=', 'other_side.transaction_journal_id')->where('other_side.amount', $joinModifier, 0); - } - ) - - ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) - ->where('transaction_journals.user_id', $this->user->id) - ->whereNull('transactions.deleted_at') - ->whereNull('transaction_journals.deleted_at') - ->whereIn('transactions.account_id', $accounts['accounts']) - ->where('transactions.amount', $selection, 0); + $query = Transaction::distinct() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->leftJoin( + 'transactions as other_side', function (JoinClause $join) use ($joinModifier) { + $join->on('transaction_journals.id', '=', 'other_side.transaction_journal_id')->where('other_side.amount', $joinModifier, 0); + } + ) + ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) + ->where('transaction_journals.user_id', $this->user->id) + ->whereNull('transactions.deleted_at') + ->whereNull('transaction_journals.deleted_at') + ->whereIn('transactions.account_id', $accounts['accounts']) + ->where('transactions.amount', $selection, 0); if (count($accounts['exclude']) > 0) { $query->whereNotIn('other_side.account_id', $accounts['exclude']); } @@ -351,90 +214,4 @@ class AccountTasker implements AccountTaskerInterface return $sum; } - /** - * - * This method will determin how much has flown (in the given period) from OR to $accounts to/from anywhere else, - * except $excluded. This could be a list of incomes, or a list of expenses. This method shows - * the name, the amount and the number of transactions. It is a summary, and only used in some reports. - * - * $incoming=true a list of incoming money (earnings) - * $incoming=false a list of outgoing money (expenses). - * - * @param array $accounts - * @param Carbon $start - * @param Carbon $end - * @param bool $incoming - * - * Opening balances are ignored. - * - * @return Collection - */ - protected function financialReport(array $accounts, Carbon $start, Carbon $end, bool $incoming): Collection - { - $collection = new Collection; - $joinModifier = $incoming ? '<' : '>'; - $selection = $incoming ? '>' : '<'; - $query = Transaction - ::distinct() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id') - ->leftJoin( - 'transactions as other_side', function (JoinClause $join) use ($joinModifier) { - $join->on('transaction_journals.id', '=', 'other_side.transaction_journal_id')->where('other_side.amount', $joinModifier, 0); - } - ) - ->leftJoin('accounts as other_account', 'other_account.id', '=', 'other_side.account_id') - ->where('transaction_types.type','!=', TransactionType::OPENING_BALANCE) - ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) - ->where('transaction_journals.user_id', $this->user->id) - ->whereNull('transactions.deleted_at') - ->whereNull('transaction_journals.deleted_at') - ->whereIn('transactions.account_id', $accounts['accounts']) - ->where('other_side.amount', '=', DB::raw('transactions.amount * -1')) - ->where('transactions.amount', $selection, 0) - ->orderBy('transactions.amount'); - - if (count($accounts['exclude']) > 0) { - $query->whereNotIn('other_side.account_id', $accounts['exclude']); - } - $set = $query->get( - [ - 'transaction_journals.id', - 'other_side.account_id', - 'other_account.name', - 'other_account.encrypted', - 'transactions.amount', - ] - ); - // summarize ourselves: - $temp = []; - foreach ($set as $entry) { - // save into $temp: - $id = intval($entry->account_id); - if (isset($temp[$id])) { - $temp[$id]['count']++; - $temp[$id]['amount'] = bcadd($temp[$id]['amount'], $entry->amount); - } - if (!isset($temp[$id])) { - $temp[$id] = [ - 'name' => intval($entry->encrypted) === 1 ? Crypt::decrypt($entry->name) : $entry->name, - 'amount' => $entry->amount, - 'count' => 1, - ]; - } - } - - // loop $temp and create collection: - foreach ($temp as $key => $entry) { - $object = new stdClass(); - $object->id = $key; - $object->name = $entry['name']; - $object->count = $entry['count']; - $object->amount = $entry['amount']; - $collection->push($object); - } - - return $collection; - } } diff --git a/app/Repositories/Account/AccountTaskerInterface.php b/app/Repositories/Account/AccountTaskerInterface.php index dcb8c10416..4ea5be74ef 100644 --- a/app/Repositories/Account/AccountTaskerInterface.php +++ b/app/Repositories/Account/AccountTaskerInterface.php @@ -14,7 +14,7 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Account; use Carbon\Carbon; -use FireflyIII\Helpers\Collection\Account as AccountCollection; +use FireflyIII\User; use Illuminate\Support\Collection; /** @@ -24,7 +24,6 @@ use Illuminate\Support\Collection; */ interface AccountTaskerInterface { - /** * @param Collection $accounts * @param Collection $excluded @@ -51,49 +50,16 @@ interface AccountTaskerInterface /** * @param Collection $accounts - * @param Collection $excluded * @param Carbon $start * @param Carbon $end * - * @see AccountTasker::financialReport() - * - * @return Collection - * + * @return array */ - public function expenseReport(Collection $accounts, Collection $excluded, Carbon $start, Carbon $end): Collection; + public function getAccountReport(Collection $accounts, Carbon $start, Carbon $end): array; /** - * @param Carbon $start - * @param Carbon $end - * @param Collection $accounts - * - * @return AccountCollection + * @param User $user */ - public function getAccountReport(Carbon $start, Carbon $end, Collection $accounts): AccountCollection; - - /** - * Experimental getJournals method. - * - * @param Collection $accounts - * @param array $types - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function getJournalsInPeriod(Collection $accounts, array $types, Carbon $start, Carbon $end): Collection; - - /** - * @param Collection $accounts - * @param Collection $excluded - * @param Carbon $start - * @param Carbon $end - * - * @see AccountTasker::financialReport() - * - * @return Collection - * - */ - public function incomeReport(Collection $accounts, Collection $excluded, Carbon $start, Carbon $end): Collection; + public function setUser(User $user); } diff --git a/app/Repositories/Attachment/AttachmentRepository.php b/app/Repositories/Attachment/AttachmentRepository.php index 065b5c0167..79a481e4ab 100644 --- a/app/Repositories/Attachment/AttachmentRepository.php +++ b/app/Repositories/Attachment/AttachmentRepository.php @@ -14,10 +14,12 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Attachment; use Carbon\Carbon; +use Crypt; use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; use FireflyIII\Models\Attachment; use FireflyIII\User; use Illuminate\Support\Collection; +use Storage; /** * Class AttachmentRepository @@ -29,16 +31,6 @@ class AttachmentRepository implements AttachmentRepositoryInterface /** @var User */ private $user; - /** - * AttachmentRepository constructor. - * - * @param User $user - */ - public function __construct(User $user) - { - $this->user = $user; - } - /** * @param Attachment $attachment * @@ -56,6 +48,19 @@ class AttachmentRepository implements AttachmentRepositoryInterface return true; } + /** + * @param Attachment $attachment + * + * @return bool + */ + public function exists(Attachment $attachment): bool + { + /** @var Storage $disk */ + $disk = Storage::disk('upload'); + + return $disk->exists($attachment->fileName()); + } + /** * @return Collection */ @@ -82,6 +87,34 @@ class AttachmentRepository implements AttachmentRepositoryInterface return $query; } + /** + * @param Attachment $attachment + * + * @return string + */ + public function getContent(Attachment $attachment): string + { + // create a disk. + $disk = Storage::disk('upload'); + $file = $attachment->fileName(); + + if ($disk->exists($file)) { + $content = Crypt::decrypt($disk->get($file)); + + return $content; + } + + return ''; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + /** * @param Attachment $attachment * @param array $data diff --git a/app/Repositories/Attachment/AttachmentRepositoryInterface.php b/app/Repositories/Attachment/AttachmentRepositoryInterface.php index 912dc67190..cc7cb887f8 100644 --- a/app/Repositories/Attachment/AttachmentRepositoryInterface.php +++ b/app/Repositories/Attachment/AttachmentRepositoryInterface.php @@ -15,6 +15,7 @@ namespace FireflyIII\Repositories\Attachment; use Carbon\Carbon; use FireflyIII\Models\Attachment; +use FireflyIII\User; use Illuminate\Support\Collection; /** @@ -32,6 +33,13 @@ interface AttachmentRepositoryInterface */ public function destroy(Attachment $attachment): bool; + /** + * @param Attachment $attachment + * + * @return bool + */ + public function exists(Attachment $attachment): bool; + /** * @return Collection */ @@ -45,6 +53,18 @@ interface AttachmentRepositoryInterface */ public function getBetween(Carbon $start, Carbon $end): Collection; + /** + * @param Attachment $attachment + * + * @return string + */ + public function getContent(Attachment $attachment): string; + + /** + * @param User $user + */ + public function setUser(User $user); + /** * @param Attachment $attachment * @param array $attachmentData diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index f086a4b19b..545a29fb96 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -22,7 +22,6 @@ use FireflyIII\Models\TransactionType; use FireflyIII\Support\CacheProperties; use FireflyIII\User; use Illuminate\Database\Query\JoinClause; -use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Log; use Navigation; @@ -38,16 +37,6 @@ class BillRepository implements BillRepositoryInterface /** @var User */ private $user; - /** - * BillRepository constructor. - * - * @param User $user - */ - public function __construct(User $user) - { - $this->user = $user; - } - /** * @param Bill $bill * @@ -67,7 +56,7 @@ class BillRepository implements BillRepositoryInterface * * @return Bill */ - public function find(int $billId) : Bill + public function find(int $billId): Bill { $bill = $this->user->bills()->find($billId); if (is_null($bill)) { @@ -84,7 +73,7 @@ class BillRepository implements BillRepositoryInterface * * @return Bill */ - public function findByName(string $name) : Bill + public function findByName(string $name): Bill { $bills = $this->user->bills()->get(['bills.*']); @@ -116,41 +105,6 @@ class BillRepository implements BillRepositoryInterface return $set; } - /** - * Returns all journals connected to these bills in the given range. Amount paid - * is stored in "journalAmount" as a negative number. - * - * @param Collection $bills - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function getAllJournalsInRange(Collection $bills, Carbon $start, Carbon $end): Collection - { - $ids = $bills->pluck('id')->toArray(); - - $set = $this->user->transactionJournals() - ->leftJoin( - 'transactions', function (JoinClause $join) { - $join->on('transactions.transaction_journal_id', '=', 'transaction_journals.id')->where('transactions.amount', '<', 0); - } - ) - ->whereIn('bill_id', $ids) - ->before($end) - ->after($start) - ->groupBy(['transaction_journals.bill_id', 'transaction_journals.id']) - ->get( - [ - 'transaction_journals.bill_id', - 'transaction_journals.id', - DB::raw('SUM(transactions.amount) AS journalAmount'), - ] - ); - - return $set; - } - /** * @return Collection */ @@ -247,7 +201,6 @@ class BillRepository implements BillRepositoryInterface $sum = bcadd($sum, $amount); Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f', $amount, $sum)); } - Log::debug('---'); } return $sum; @@ -281,36 +234,11 @@ class BillRepository implements BillRepositoryInterface $sum = bcadd($sum, $multi); Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f', $multi, $sum)); } - Log::debug('---'); } return $sum; } - /** - * This method also returns the amount of the journal in "journalAmount" - * for easy access. - * - * @param Bill $bill - * - * @param int $page - * @param int $pageSize - * - * @return LengthAwarePaginator|Collection - */ - public function getJournals(Bill $bill, int $page, int $pageSize = 50): LengthAwarePaginator - { - $offset = ($page - 1) * $pageSize; - $query = $bill->transactionJournals() - ->expanded() - ->sortCorrectly(); - $count = $query->count(); - $set = $query->take($pageSize)->offset($offset)->get(TransactionJournal::queryFields()); - $paginator = new LengthAwarePaginator($set, $count, $pageSize, $page); - - return $paginator; - } - /** * @param Bill $bill * @@ -582,6 +510,14 @@ class BillRepository implements BillRepositoryInterface } + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + /** * @param array $data * diff --git a/app/Repositories/Bill/BillRepositoryInterface.php b/app/Repositories/Bill/BillRepositoryInterface.php index 26e5f22aef..2e07d12d93 100644 --- a/app/Repositories/Bill/BillRepositoryInterface.php +++ b/app/Repositories/Bill/BillRepositoryInterface.php @@ -16,7 +16,7 @@ namespace FireflyIII\Repositories\Bill; use Carbon\Carbon; use FireflyIII\Models\Bill; use FireflyIII\Models\TransactionJournal; -use Illuminate\Pagination\LengthAwarePaginator; +use FireflyIII\User; use Illuminate\Support\Collection; /** @@ -26,7 +26,6 @@ use Illuminate\Support\Collection; */ interface BillRepositoryInterface { - /** * @param Bill $bill * @@ -41,7 +40,7 @@ interface BillRepositoryInterface * * @return Bill */ - public function find(int $billId) : Bill; + public function find(int $billId): Bill; /** * Find a bill by name. @@ -50,25 +49,13 @@ interface BillRepositoryInterface * * @return Bill */ - public function findByName(string $name) : Bill; + public function findByName(string $name): Bill; /** * @return Collection */ public function getActiveBills(): Collection; - /** - * Returns all journals connected to these bills in the given range. Amount paid - * is stored in "journalAmount" as a negative number. - * - * @param Collection $bills - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function getAllJournalsInRange(Collection $bills, Carbon $start, Carbon $end): Collection; - /** * @return Collection */ @@ -103,16 +90,6 @@ interface BillRepositoryInterface */ public function getBillsUnpaidInRange(Carbon $start, Carbon $end): string; - /** - * @param Bill $bill - * - * @param int $page - * @param int $pageSize - * - * @return LengthAwarePaginator - */ - public function getJournals(Bill $bill, int $page, int $pageSize = 50): LengthAwarePaginator; - /** * @param Bill $bill * @@ -182,6 +159,11 @@ interface BillRepositoryInterface */ public function scan(Bill $bill, TransactionJournal $journal): bool; + /** + * @param User $user + */ + public function setUser(User $user); + /** * @param array $data * diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index b674435881..9b4f985390 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -14,18 +14,19 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; -use FireflyIII\Events\StoredBudgetLimit; -use FireflyIII\Events\UpdatedBudgetLimit; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Models\AvailableBudget; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; -use FireflyIII\Models\LimitRepetition; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\User; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; +use Navigation; +use stdClass; /** * Class BudgetRepository @@ -37,16 +38,6 @@ class BudgetRepository implements BudgetRepositoryInterface /** @var User */ private $user; - /** - * BudgetRepository constructor. - * - * @param User $user - */ - public function __construct(User $user) - { - $this->user = $user; - } - /** * @return bool */ @@ -71,6 +62,39 @@ class BudgetRepository implements BudgetRepositoryInterface return true; } + /** + * Filters entries from the result set generated by getBudgetPeriodReport + * + * @param Collection $set + * @param int $budgetId + * @param array $periods + * + * @return array + */ + public function filterAmounts(Collection $set, int $budgetId, array $periods): array + { + $arr = []; + $keys = array_keys($periods); + foreach ($keys as $period) { + /** @var stdClass $object */ + $result = $set->filter( + function (TransactionJournal $object) use ($budgetId, $period) { + $result = strval($object->period_marker) === strval($period) && $budgetId === intval($object->budget_id); + + return $result; + } + ); + $amount = '0'; + if (!is_null($result->first())) { + $amount = $result->first()->sum_of_period; + } + + $arr[$period] = $amount; + } + + return $arr; + } + /** * Find a budget. * @@ -160,20 +184,150 @@ class BudgetRepository implements BudgetRepositoryInterface * * @return Collection */ - public function getAllBudgetLimitRepetitions(Carbon $start, Carbon $end): Collection + public function getAllBudgetLimits(Carbon $start, Carbon $end): Collection { - $query = LimitRepetition:: - leftJoin('budget_limits', 'limit_repetitions.budget_limit_id', '=', 'budget_limits.id') - ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') - ->where('limit_repetitions.startdate', '<=', $end->format('Y-m-d 00:00:00')) - ->where('limit_repetitions.startdate', '>=', $start->format('Y-m-d 00:00:00')) - ->where('budgets.user_id', $this->user->id); - - $set = $query->get(['limit_repetitions.*', 'budget_limits.budget_id']); + $set = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') + ->with(['budget']) + ->where('budgets.user_id', $this->user->id) + ->where( + function (Builder $q5) use ($start, $end) { + $q5->where( + function (Builder $q1) use ($start, $end) { + $q1->where( + function (Builder $q2) use ($start, $end) { + $q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d 00:00:00')); + $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 00:00:00')); + } + ) + ->orWhere( + function (Builder $q3) use ($start, $end) { + $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00')); + $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 00:00:00')); + } + ); + } + ) + ->orWhere( + function (Builder $q4) use ($start, $end) { + // or start is before start AND end is after end. + $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 00:00:00')); + $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d 00:00:00')); + } + ); + } + )->get(['budget_limits.*']); return $set; } + /** + * @param TransactionCurrency $currency + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function getAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end): string + { + $amount = '0'; + $availableBudget = $this->user->availableBudgets() + ->where('transaction_currency_id', $currency->id) + ->where('start_date', $start->format('Y-m-d')) + ->where('end_date', $end->format('Y-m-d'))->first(); + if (!is_null($availableBudget)) { + $amount = strval($availableBudget->amount); + } + + return $amount; + } + + /** + * @param Budget $budget + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function getBudgetLimits(Budget $budget, Carbon $start, Carbon $end): Collection + { + $set = $budget->budgetLimits() + ->where( + function (Builder $q5) use ($start, $end) { + $q5->where( + function (Builder $q1) use ($start, $end) { + // budget limit ends within period + $q1->where( + function (Builder $q2) use ($start, $end) { + $q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d 00:00:00')); + $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 00:00:00')); + } + ) + // budget limit start within period + ->orWhere( + function (Builder $q3) use ($start, $end) { + $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00')); + $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 00:00:00')); + } + ); + } + ) + ->orWhere( + function (Builder $q4) use ($start, $end) { + // or start is before start AND end is after end. + $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 00:00:00')); + $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d 00:00:00')); + } + ); + } + )->orderBy('budget_limits.start_date', 'DESC')->get(['budget_limits.*']); + + return $set; + } + + /** + * This method is being used to generate the budget overview in the year/multi-year report. Its used + * in both the year/multi-year budget overview AND in the accompanying chart. + * + * @param Collection $budgets + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function getBudgetPeriodReport(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): array + { + $carbonFormat = Navigation::preferredCarbonFormat($start, $end); + $data = []; + // prep data array: + /** @var Budget $budget */ + foreach ($budgets as $budget) { + $data[$budget->id] = [ + 'name' => $budget->name, + 'sum' => '0', + 'entries' => [], + ]; + } + + // get all transactions: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end); + $collector->setBudgets($budgets); + $transactions = $collector->getJournals(); + + // loop transactions: + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $budgetId = max(intval($transaction->transaction_journal_budget_id), intval($transaction->transaction_budget_id)); + $date = $transaction->date->format($carbonFormat); + $data[$budgetId]['entries'][$date] = bcadd($data[$budgetId]['entries'][$date] ?? '0', $transaction->transaction_amount); + } + + return $data; + + } + /** * @return Collection */ @@ -208,74 +362,101 @@ class BudgetRepository implements BudgetRepositoryInterface return $set; } + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array + { + $carbonFormat = Navigation::preferredCarbonFormat($start, $end); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end); + $collector->setTypes([TransactionType::WITHDRAWAL]); + $collector->withoutBudget(); + $transactions = $collector->getJournals(); + $result = [ + 'entries' => [], + 'name' => strval(trans('firefly.no_budget')), + 'sum' => '0', + ]; + + foreach ($transactions as $transaction) { + $date = $transaction->date->format($carbonFormat); + + if (!isset($result['entries'][$date])) { + $result['entries'][$date] = '0'; + } + $result['entries'][$date] = bcadd($result['entries'][$date], $transaction->transaction_amount); + } + + return $result; + } + + /** + * @param TransactionCurrency $currency + * @param Carbon $start + * @param Carbon $end + * @param string $amount + * + * @return bool + */ + public function setAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end, string $amount): bool + { + $availableBudget = $this->user->availableBudgets() + ->where('transaction_currency_id', $currency->id) + ->where('start_date', $start->format('Y-m-d')) + ->where('end_date', $end->format('Y-m-d'))->first(); + if (is_null($availableBudget)) { + $availableBudget = new AvailableBudget; + $availableBudget->user()->associate($this->user); + $availableBudget->transactionCurrency()->associate($currency); + $availableBudget->start_date = $start; + $availableBudget->end_date = $end; + } + $availableBudget->amount = $amount; + $availableBudget->save(); + + return true; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + /** * @param Collection $budgets * @param Collection $accounts * @param Carbon $start * @param Carbon $end * - * @return Collection + * @return string */ - public function journalsInPeriod(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): Collection + public function spentInPeriod(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): string { - $return = new Collection; - $accountIds = []; - // expand the number of grabbed fields: - $fields = TransactionJournal::queryFields(); - $fields[] = 'source.account_id'; + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setBudgets($budgets); + if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); + $collector->setAccounts($accounts); + } + if ($accounts->count() === 0) { + $collector->setAllAssetAccounts(); } - // first get all journals for all budget(s): - $journalQuery = $this->user->transactionJournals() - ->expanded() - ->sortCorrectly() - ->before($end) - ->after($start) - ->leftJoin( - 'transactions as source', - function (JoinClause $join) { - $join->on('source.transaction_journal_id', '=', 'transaction_journals.id')->where('source.amount', '<', '0'); - } - ) - ->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') - ->whereIn('budget_transaction_journal.budget_id', $budgets->pluck('id')->toArray()); - // add account id's, if relevant: - if (count($accountIds) > 0) { - $journalQuery->whereIn('source.account_id', $accountIds); - } - // get them: - $journals = $journalQuery->get(TransactionJournal::queryFields()); + $set = $collector->getJournals(); + $sum = strval($set->sum('transaction_amount')); - // then get transactions themselves. - $transactionQuery = $this->user->transactionJournals() - ->expanded() - ->before($end) - ->sortCorrectly() - ->after($start) - ->leftJoin('transactions as related', 'related.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('budget_transaction', 'budget_transaction.transaction_id', '=', 'related.id') - ->leftJoin( - 'transactions as source', - function (JoinClause $join) { - $join->on('source.transaction_journal_id', '=', 'transaction_journals.id')->where('source.amount', '<', '0'); - } - ) - ->groupBy(['source.account_id']) - ->whereIn('budget_transaction.budget_id', $budgets->pluck('id')->toArray()); - - if (count($accountIds) > 0) { - $transactionQuery->whereIn('source.account_id', $accountIds); - } - - $transactions = $transactionQuery->get($fields); - - // return complete set: - $return = $return->merge($transactions); - $return = $return->merge($journals); - - return $return; + return $sum; } /** @@ -283,210 +464,34 @@ class BudgetRepository implements BudgetRepositoryInterface * @param Carbon $start * @param Carbon $end * - * @return Collection + * @return string */ - public function journalsInPeriodWithoutBudget(Collection $accounts, Carbon $start, Carbon $end): Collection + public function spentInPeriodWoBudget(Collection $accounts, Carbon $start, Carbon $end): string { - $accountIds = []; + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutBudget(); + if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); + $collector->setAccounts($accounts); + } + if ($accounts->count() === 0) { + $collector->setAllAssetAccounts(); } - /** @var Collection $set */ - $query = $this->user - ->transactionJournals() - ->expanded() - ->sortCorrectly() - ->transactionTypes([TransactionType::WITHDRAWAL]) - ->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') - ->whereNull('budget_transaction_journal.id') - ->leftJoin( - 'transactions as source', - function (JoinClause $join) { - $join->on('source.transaction_journal_id', '=', 'transaction_journals.id')->where('source.amount', '<', '0'); - } - ) - ->before($end) - ->after($start)->with( - [ - 'transactions' => function (HasMany $query) { - $query->where('transactions.amount', '<', 0); - }, - 'transactions.budgets', - ] - ); - - // add account id's, if relevant: - if (count($accountIds) > 0) { - $query->whereIn('source.account_id', $accountIds); - } - - $set = $query->get(TransactionJournal::queryFields()); + $set = $collector->getJournals(); $set = $set->filter( - function (TransactionJournal $journal) { - foreach ($journal->transactions as $t) { - if ($t->budgets->count() === 0) { - return true; - } + function (Transaction $transaction) { + if (bccomp($transaction->transaction_amount, '0') === -1) { + return $transaction; } - return false; + return null; } ); - return $set; - } - - /** - * @param Collection $budgets - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end - * - * @return string - */ - public function spentInPeriod(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end) : string - { - // first collect actual transaction journals (fairly easy) - $query = $this->user - ->transactionJournals() - ->leftJoin( - 'transactions as source', function (JoinClause $join) { - $join->on('source.transaction_journal_id', '=', 'transaction_journals.id')->where('source.amount', '<', 0); - } - ) - ->leftJoin( - 'transactions as destination', function (JoinClause $join) { - $join->on('destination.transaction_journal_id', '=', 'transaction_journals.id')->where('destination.amount', '>', 0); - } - ); - $query->whereNull('source.deleted_at'); - $query->whereNull('destination.deleted_at'); - $query->where('transaction_journals.completed', 1); - - if ($end >= $start) { - $query->before($end)->after($start); - } - if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); - $query->where( - // source.account_id in accountIds XOR destination.account_id in accountIds - function (Builder $query) use ($accountIds) { - $query->where( - function (Builder $q1) use ($accountIds) { - $q1->whereIn('source.account_id', $accountIds) - ->whereNotIn('destination.account_id', $accountIds); - } - )->orWhere( - function (Builder $q2) use ($accountIds) { - $q2->whereIn('destination.account_id', $accountIds) - ->whereNotIn('source.account_id', $accountIds); - } - ); - } - ); - } - if ($budgets->count() > 0) { - $budgetIds = $budgets->pluck('id')->toArray(); - $query->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); - $query->whereIn('budget_transaction_journal.budget_id', $budgetIds); - - } - - // that should do it: - $ids = $query->distinct()->get(['transaction_journals.id'])->pluck('id')->toArray(); - $first = '0'; - if (count($ids) > 0) { - $first = strval( - $this->user->transactions() - ->whereIn('transaction_journal_id', $ids) - ->where('amount', '<', '0') - ->whereNull('transactions.deleted_at') - ->sum('amount') - ); - } - // then collection transactions (harder) - $query = $this->user->transactions() - ->where('transactions.amount', '<', 0) - ->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d 23:59:59')); - if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); - $query->whereIn('transactions.account_id', $accountIds); - } - if ($budgets->count() > 0) { - $budgetIds = $budgets->pluck('id')->toArray(); - $query->leftJoin('budget_transaction', 'budget_transaction.transaction_id', '=', 'transactions.id'); - $query->whereIn('budget_transaction.budget_id', $budgetIds); - } - $second = strval($query->sum('transactions.amount')); - - return bcadd($first, $second); - } - - /** - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end - * - * @return string - */ - public function spentInPeriodWithoutBudget(Collection $accounts, Carbon $start, Carbon $end): string - { - $types = [TransactionType::WITHDRAWAL]; - $query = $this->user->transactionJournals() - ->distinct() - ->transactionTypes($types) - ->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin( - 'transactions as source', function (JoinClause $join) { - $join->on('source.transaction_journal_id', '=', 'transaction_journals.id')->where('source.amount', '<', 0); - } - ) - ->leftJoin( - 'transactions as destination', function (JoinClause $join) { - $join->on('destination.transaction_journal_id', '=', 'transaction_journals.id')->where('destination.amount', '>', 0); - } - ) - ->leftJoin('budget_transaction', 'source.id', '=', 'budget_transaction.transaction_id') - ->whereNull('budget_transaction_journal.id') - ->whereNull('budget_transaction.id') - ->before($end) - ->after($start) - ->whereNull('source.deleted_at') - ->whereNull('destination.deleted_at') - ->where('transaction_journals.completed', 1); - - if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); - $query->where( - // source.account_id in accountIds XOR destination.account_id in accountIds - function (Builder $sourceXorDestinationQuery) use ($accountIds) { - $sourceXorDestinationQuery->where( - function (Builder $inSourceButNotDestinationQuery) use ($accountIds) { - $inSourceButNotDestinationQuery->whereIn('source.account_id', $accountIds) - ->whereNotIn('destination.account_id', $accountIds); - } - )->orWhere( - function (Builder $inDestinationButNotSourceQuery) use ($accountIds) { - $inDestinationButNotSourceQuery->whereIn('destination.account_id', $accountIds) - ->whereNotIn('source.account_id', $accountIds); - } - ); - } - ); - } - $ids = $query->get(['transaction_journals.id'])->pluck('id')->toArray(); - $sum = '0'; - if (count($ids) > 0) { - $sum = strval( - $this->user->transactions() - ->whereIn('transaction_journal_id', $ids) - ->where('amount', '<', '0') - ->whereNull('transactions.deleted_at') - ->sum('amount') - ); - } + $sum = strval($set->sum('transaction_amount')); return $sum; } @@ -529,19 +534,18 @@ class BudgetRepository implements BudgetRepositoryInterface * @param Budget $budget * @param Carbon $start * @param Carbon $end - * @param string $range * @param int $amount * * @return BudgetLimit */ - public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $range, int $amount) : BudgetLimit + public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, int $amount): BudgetLimit { - // there might be a budget limit for this startdate: - $repeatFreq = config('firefly.range_to_repeat_freq.' . $range); + // there might be a budget limit for these dates: /** @var BudgetLimit $limit */ $limit = $budget->budgetlimits() - ->where('budget_limits.startdate', $start) - ->where('budget_limits.repeat_freq', $repeatFreq)->first(['budget_limits.*']); + ->where('budget_limits.start_date', $start->format('Y-m-d')) + ->where('budget_limits.end_date', $end->format('Y-m-d')) + ->first(['budget_limits.*']); // delete if amount is zero. if (!is_null($limit) && $amount <= 0.0) { @@ -554,26 +558,16 @@ class BudgetRepository implements BudgetRepositoryInterface $limit->amount = $amount; $limit->save(); - // fire event to create or update LimitRepetition. - event(new UpdatedBudgetLimit($limit, $end)); - return $limit; } - // create one and return it. + // or create one and return it. $limit = new BudgetLimit; $limit->budget()->associate($budget); - $limit->startdate = $start; - $limit->amount = $amount; - $limit->repeat_freq = $repeatFreq; - $limit->repeats = 0; + $limit->start_date = $start; + $limit->end_date = $end; + $limit->amount = $amount; $limit->save(); - event(new StoredBudgetLimit($limit, $end)); - - - // likewise, there should be a limit repetition to match the end date - // (which is always the end of the month) but that is caught by an event. - // so handled automatically. return $limit; } diff --git a/app/Repositories/Budget/BudgetRepositoryInterface.php b/app/Repositories/Budget/BudgetRepositoryInterface.php index 0f6aba141e..906a4827ae 100644 --- a/app/Repositories/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetRepositoryInterface.php @@ -16,6 +16,8 @@ namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\User; use Illuminate\Support\Collection; /** @@ -25,7 +27,6 @@ use Illuminate\Support\Collection; */ interface BudgetRepositoryInterface { - /** * @return bool */ @@ -38,6 +39,17 @@ interface BudgetRepositoryInterface */ public function destroy(Budget $budget): bool; + /** + * Filters entries from the result set generated by getBudgetPeriodReport + * + * @param Collection $set + * @param int $budgetId + * @param array $periods + * + * @return array + */ + public function filterAmounts(Collection $set, int $budgetId, array $periods): array; + /** * Find a budget. * @@ -77,7 +89,36 @@ interface BudgetRepositoryInterface * * @return Collection */ - public function getAllBudgetLimitRepetitions(Carbon $start, Carbon $end): Collection; + public function getAllBudgetLimits(Carbon $start, Carbon $end): Collection; + + /** + * @param TransactionCurrency $currency + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function getAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end): string; + + /** + * @param Budget $budget + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function getBudgetLimits(Budget $budget, Carbon $start, Carbon $end): Collection; + + /** + * + * @param Collection $budgets + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function getBudgetPeriodReport(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): array; /** * @return Collection @@ -90,23 +131,28 @@ interface BudgetRepositoryInterface public function getInactiveBudgets(): Collection; /** - * @param Collection $budgets * @param Collection $accounts * @param Carbon $start * @param Carbon $end * - * @return Collection + * @return array */ - public function journalsInPeriod(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): Collection; + public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array; /** - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end + * @param TransactionCurrency $currency + * @param Carbon $start + * @param Carbon $end + * @param string $amount * - * @return Collection + * @return bool */ - public function journalsInPeriodWithoutBudget(Collection $accounts, Carbon $start, Carbon $end): Collection; + public function setAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end, string $amount): bool; + + /** + * @param User $user + */ + public function setUser(User $user); /** * @param Collection $budgets @@ -116,7 +162,7 @@ interface BudgetRepositoryInterface * * @return string */ - public function spentInPeriod(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end) : string; + public function spentInPeriod(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): string; /** * @param Collection $accounts @@ -125,7 +171,7 @@ interface BudgetRepositoryInterface * * @return string */ - public function spentInPeriodWithoutBudget(Collection $accounts, Carbon $start, Carbon $end): string; + public function spentInPeriodWoBudget(Collection $accounts, Carbon $start, Carbon $end): string; /** * @param array $data @@ -140,17 +186,16 @@ interface BudgetRepositoryInterface * * @return Budget */ - public function update(Budget $budget, array $data) : Budget; + public function update(Budget $budget, array $data): Budget; /** * @param Budget $budget * @param Carbon $start * @param Carbon $end - * @param string $range * @param int $amount * * @return BudgetLimit */ - public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $range, int $amount) : BudgetLimit; + public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, int $amount): BudgetLimit; } diff --git a/app/Repositories/Category/CategoryRepository.php b/app/Repositories/Category/CategoryRepository.php index 6880813805..bed2d5847f 100644 --- a/app/Repositories/Category/CategoryRepository.php +++ b/app/Repositories/Category/CategoryRepository.php @@ -14,14 +14,15 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Category; use Carbon\Carbon; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Models\Category; +use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\User; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Query\JoinClause; -use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; +use Log; +use Navigation; /** * Class CategoryRepository @@ -33,16 +34,6 @@ class CategoryRepository implements CategoryRepositoryInterface /** @var User */ private $user; - /** - * CategoryRepository constructor. - * - * @param User $user - */ - public function __construct(User $user) - { - $this->user = $user; - } - /** * @param Category $category * @@ -65,24 +56,12 @@ class CategoryRepository implements CategoryRepositoryInterface */ public function earnedInPeriod(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): string { - $types = [TransactionType::DEPOSIT, TransactionType::TRANSFER]; - $sum = bcmul($this->sumInPeriod($categories, $accounts, $types, $start, $end), '-1'); - - return $sum; - - } - - /** - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end - * - * @return string - */ - public function earnedInPeriodWithoutCategory(Collection $accounts, Carbon $start, Carbon $end) :string - { - $types = [TransactionType::DEPOSIT, TransactionType::TRANSFER]; - $sum = $this->sumInPeriodWithoutCategory($accounts, $types, $start, $end); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($accounts)->setCategories($categories); + $set = $collector->getJournals(); + $sum = strval($set->sum('transaction_amount')); return $sum; } @@ -94,7 +73,7 @@ class CategoryRepository implements CategoryRepositoryInterface * * @return Category */ - public function find(int $categoryId) : Category + public function find(int $categoryId): Category { $category = $this->user->categories()->find($categoryId); if (is_null($category)) { @@ -111,7 +90,7 @@ class CategoryRepository implements CategoryRepositoryInterface * * @return Category */ - public function findByName(string $name) : Category + public function findByName(string $name): Category { $categories = $this->user->categories()->get(['categories.*']); foreach ($categories as $category) { @@ -132,27 +111,28 @@ class CategoryRepository implements CategoryRepositoryInterface { $first = null; - - /** @var TransactionJournal $first */ + /** @var TransactionJournal $firstJournal */ $firstJournal = $category->transactionJournals()->orderBy('date', 'ASC')->first(['transaction_journals.date']); if ($firstJournal) { $first = $firstJournal->date; } - // check transactions: - $firstTransaction = $category->transactions() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->orderBy('transaction_journals.date', 'ASC')->first(['transaction_journals.date']); - if (!is_null($firstTransaction) && ((!is_null($first) && $firstTransaction->date < $first) || is_null($first))) { - $first = new Carbon($firstTransaction->date); + + // both exist, the one that is earliest "wins". + if (!is_null($firstTransaction) && !is_null($first) && Carbon::parse($firstTransaction->date)->lt($first)) { + $first = $firstTransaction->date; } + if (is_null($first)) { return new Carbon('1900-01-01'); } + return $first; } @@ -174,177 +154,6 @@ class CategoryRepository implements CategoryRepositoryInterface return $set; } - /** - * @param Category $category - * @param int $page - * @param int $pageSize - * - * @return LengthAwarePaginator - */ - public function getJournals(Category $category, int $page, int $pageSize): LengthAwarePaginator - { - $complete = new Collection; - // first collect actual transaction journals (fairly easy) - $query = $this->user->transactionJournals()->expanded()->sortCorrectly(); - $query->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); - $query->where('category_transaction_journal.category_id', $category->id); - $first = $query->get(TransactionJournal::queryFields()); - - // then collection transactions (harder) - $query = $this->user->transactionJournals()->distinct() - ->leftJoin('transactions', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->leftJoin('category_transaction', 'category_transaction.transaction_id', '=', 'transactions.id') - ->where('category_transaction.category_id', $category->id); - $second = $query->get(['transaction_journals.*']); - - $complete = $complete->merge($first); - $complete = $complete->merge($second); - - // sort: - /** @var Collection $complete */ - $complete = $complete->sortByDesc( - function ($model) { - $date = new Carbon($model->date); - - return intval($date->format('U')); - } - ); - // create paginator - $offset = ($page - 1) * $pageSize; - $subSet = $complete->slice($offset, $pageSize)->all(); - $paginator = new LengthAwarePaginator($subSet, $complete->count(), $pageSize, $page); - - return $paginator; - } - - /** - * Get all transactions in a category in a range. - * - * @param Collection $categories - * @param Collection $accounts - * @param array $types - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function journalsInPeriod(Collection $categories, Collection $accounts, array $types, Carbon $start, Carbon $end): Collection - { - $complete = new Collection; - // first collect actual transaction journals (fairly easy) - $query = $this->user->transactionJournals()->expanded()->sortCorrectly(); - - if ($end >= $start) { - $query->before($end)->after($start); - } - - if (count($types) > 0) { - $query->transactionTypes($types); - } - - if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); - $query->leftJoin('transactions as t', 't.transaction_journal_id', '=', 'transaction_journals.id'); - $query->whereIn('t.account_id', $accountIds); - } - if ($categories->count() > 0) { - $categoryIds = $categories->pluck('id')->toArray(); - $query->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); - $query->whereIn('category_transaction_journal.category_id', $categoryIds); - } - - // that should do it: - $first = $query->get(TransactionJournal::queryFields()); - - - // then collection transactions (harder) - $query = $this->user->transactionJournals()->distinct() - ->leftJoin('transactions', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->leftJoin('category_transaction', 'category_transaction.transaction_id', '=', 'transactions.id'); - - if (count($types) > 0) { - $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); - $query->whereIn('transaction_types.type', $types); - } - if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); - $query->whereIn('transactions.account_id', $accountIds); - } - if ($categories->count() > 0) { - $categoryIds = $categories->pluck('id')->toArray(); - $query->whereIn('category_transaction.category_id', $categoryIds); - } - - - $second = $query->get(['transaction_journals.*','transaction_types.type as transaction_type_type']); - - $complete = $complete->merge($first); - $complete = $complete->merge($second); - - return $complete; - } - - /** - * @param Collection $accounts - * @param array $types - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function journalsInPeriodWithoutCategory(Collection $accounts, array $types, Carbon $start, Carbon $end) : Collection - { - /** @var Collection $set */ - $query = $this->user - ->transactionJournals(); - if (count($types) > 0) { - $query->transactionTypes($types); - } - - $query->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') - ->whereNull('category_transaction_journal.id') - ->before($end) - ->after($start); - - if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); - $query->leftJoin('transactions as t', 't.transaction_journal_id', '=', 'transaction_journals.id'); - $query->whereIn('t.account_id', $accountIds); - } - - $set = $query->get(['transaction_journals.*']); - - if ($set->count() == 0) { - return new Collection; - } - - // grab all the transactions from this set. - // take only the journals with transactions that all have no category. - // select transactions left join journals where id in this set - // and left join transaction-category where null category - $journalIds = $set->pluck('id')->toArray(); - $secondQuery = $this->user->transactions() - ->leftJoin('category_transaction', 'category_transaction.transaction_id', '=', 'transactions.id') - ->whereNull('category_transaction.id') - ->whereIn('transaction_journals.id', $journalIds); - - if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); - $secondQuery->whereIn('transactions.account_id', $accountIds); - } - - // this second set REALLY doesn't have any categories. - $secondSet = $secondQuery->get(['transactions.transaction_journal_id']); - $allIds = $secondSet->pluck('transaction_journal_id')->toArray(); - $return = $this->user->transactionJournals()->sortCorrectly()->expanded()->whereIn('transaction_journals.id', $allIds)->get( - TransactionJournal::queryFields() - ); - - return $return; - - - } - /** * @param Category $category * @param Collection $accounts @@ -394,6 +203,186 @@ class CategoryRepository implements CategoryRepositoryInterface return $last; } + /** + * @param Collection $categories + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function periodExpenses(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array + { + $carbonFormat = Navigation::preferredCarbonFormat($start, $end); + $data = []; + // prep data array: + /** @var Category $category */ + foreach ($categories as $category) { + $data[$category->id] = [ + 'name' => $category->name, + 'sum' => '0', + 'entries' => [], + ]; + } + + // get all transactions: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end); + $collector->setCategories($categories)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) + ->withOpposingAccount() + ->enableInternalFilter(); + $transactions = $collector->getJournals(); + + // loop transactions: + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + // if positive, skip: + if (bccomp($transaction->transaction_amount, '0') === 1) { + continue; + } + $categoryId = max(intval($transaction->transaction_journal_category_id), intval($transaction->transaction_category_id)); + $date = $transaction->date->format($carbonFormat); + $data[$categoryId]['entries'][$date] = bcadd($data[$categoryId]['entries'][$date] ?? '0', $transaction->transaction_amount); + } + + return $data; + } + + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function periodExpensesNoCategory(Collection $accounts, Carbon $start, Carbon $end): array + { + $carbonFormat = Navigation::preferredCarbonFormat($start, $end); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->withOpposingAccount(); + $collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])->enableInternalFilter(); + $collector->withoutCategory(); + $transactions = $collector->getJournals(); + $result = [ + 'entries' => [], + 'name' => strval(trans('firefly.no_category')), + 'sum' => '0', + ]; + + foreach ($transactions as $transaction) { + // if positive, skip: + if (bccomp($transaction->transaction_amount, '0') === 1) { + continue; + } + $date = $transaction->date->format($carbonFormat); + + if (!isset($result['entries'][$date])) { + $result['entries'][$date] = '0'; + } + $result['entries'][$date] = bcadd($result['entries'][$date], $transaction->transaction_amount); + } + + return $result; + } + + /** + * @param Collection $categories + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function periodIncome(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array + { + $carbonFormat = Navigation::preferredCarbonFormat($start, $end); + $data = []; + // prep data array: + /** @var Category $category */ + foreach ($categories as $category) { + $data[$category->id] = [ + 'name' => $category->name, + 'sum' => '0', + 'entries' => [], + ]; + } + + // get all transactions: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end); + $collector->setCategories($categories)->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) + ->withOpposingAccount() + ->enableInternalFilter(); + $transactions = $collector->getJournals(); + + // loop transactions: + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + // if negative, skip: + if (bccomp($transaction->transaction_amount, '0') === -1) { + continue; + } + $categoryId = max(intval($transaction->transaction_journal_category_id), intval($transaction->transaction_category_id)); + $date = $transaction->date->format($carbonFormat); + $data[$categoryId]['entries'][$date] = bcadd($data[$categoryId]['entries'][$date] ?? '0', $transaction->transaction_amount); + } + + return $data; + } + + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function periodIncomeNoCategory(Collection $accounts, Carbon $start, Carbon $end): array + { + Log::debug('Now in periodIncomeNoCategory()'); + $carbonFormat = Navigation::preferredCarbonFormat($start, $end); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->withOpposingAccount(); + $collector->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])->enableInternalFilter(); + $collector->withoutCategory(); + $transactions = $collector->getJournals(); + $result = [ + 'entries' => [], + 'name' => strval(trans('firefly.no_category')), + 'sum' => '0', + ]; + Log::debug('Looping transactions..'); + foreach ($transactions as $transaction) { + + // if negative, skip: + if (bccomp($transaction->transaction_amount, '0') === -1) { + continue; + } + $date = $transaction->date->format($carbonFormat); + + if (!isset($result['entries'][$date])) { + $result['entries'][$date] = '0'; + } + $result['entries'][$date] = bcadd($result['entries'][$date], $transaction->transaction_amount); + } + Log::debug('Done looping transactions..'); + Log::debug('Finished periodIncomeNoCategory()'); + + return $result; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + /** * @param Collection $categories * @param Collection $accounts @@ -404,8 +393,22 @@ class CategoryRepository implements CategoryRepositoryInterface */ public function spentInPeriod(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): string { - $types = [TransactionType::WITHDRAWAL, TransactionType::TRANSFER]; - $sum = $this->sumInPeriod($categories, $accounts, $types, $start, $end); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setCategories($categories); + + + if ($accounts->count() > 0) { + $collector->setAccounts($accounts); + } + if ($accounts->count() === 0) { + $collector->setAllAssetAccounts(); + } + + + $set = $collector->getJournals(); + $sum = strval($set->sum('transaction_amount')); return $sum; } @@ -417,10 +420,32 @@ class CategoryRepository implements CategoryRepositoryInterface * * @return string */ - public function spentInPeriodWithoutCategory(Collection $accounts, Carbon $start, Carbon $end) : string + public function spentInPeriodWithoutCategory(Collection $accounts, Carbon $start, Carbon $end): string { - $types = [TransactionType::WITHDRAWAL, TransactionType::TRANSFER]; - $sum = $this->sumInPeriodWithoutCategory($accounts, $types, $start, $end); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutCategory(); + + if ($accounts->count() > 0) { + $collector->setAccounts($accounts); + } + if ($accounts->count() === 0) { + $collector->setAllAssetAccounts(); + } + + $set = $collector->getJournals(); + $set = $set->filter( + function (Transaction $transaction) { + if (bccomp($transaction->transaction_amount, '0') === -1) { + return $transaction; + } + + return null; + } + ); + + $sum = strval($set->sum('transaction_amount')); return $sum; } @@ -458,121 +483,4 @@ class CategoryRepository implements CategoryRepositoryInterface return $category; } - /** - * @param Collection $categories - * @param Collection $accounts - * @param array $types - * @param Carbon $start - * @param Carbon $end - * - * @return string - */ - private function sumInPeriod(Collection $categories, Collection $accounts, array $types, Carbon $start, Carbon $end): string - { - // first collect actual transaction journals (fairly easy) - $query = $this->user - ->transactionJournals() - ->transactionTypes($types) - ->leftJoin( - 'transactions as source', function (JoinClause $join) { - $join->on('source.transaction_journal_id', '=', 'transaction_journals.id')->where('source.amount', '<', 0); - } - ) - ->leftJoin( - 'transactions as destination', function (JoinClause $join) { - $join->on('destination.transaction_journal_id', '=', 'transaction_journals.id')->where('destination.amount', '>', 0); - } - ); - - if ($end >= $start) { - $query->before($end)->after($start); - } - if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); - $query->where( - // source.account_id in accountIds XOR destination.account_id in accountIds - function (Builder $query) use ($accountIds) { - $query->where( - function (Builder $q1) use ($accountIds) { - $q1->whereIn('source.account_id', $accountIds) - ->whereNotIn('destination.account_id', $accountIds); - } - )->orWhere( - function (Builder $q2) use ($accountIds) { - $q2->whereIn('destination.account_id', $accountIds) - ->whereNotIn('source.account_id', $accountIds); - } - ); - } - ); - } - if ($categories->count() > 0) { - $categoryIds = $categories->pluck('id')->toArray(); - $query->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); - $query->whereIn('category_transaction_journal.category_id', $categoryIds); - } - - // that should do it: - $first = strval($query->sum('source.amount')); - - // then collection transactions (harder) - $query = $this->user->transactions() - ->where('transactions.amount', '<', 0) - ->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d 23:59:59')); - if (count($types) > 0) { - $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); - $query->whereIn('transaction_types.type', $types); - } - if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); - $query->whereIn('transactions.account_id', $accountIds); - } - if ($categories->count() > 0) { - $categoryIds = $categories->pluck('id')->toArray(); - $query->leftJoin('category_transaction', 'category_transaction.transaction_id', '=', 'transactions.id'); - $query->whereIn('category_transaction.category_id', $categoryIds); - } - $second = strval($query->sum('transactions.amount')); - - return bcadd($first, $second); - - } - - /** - * @param Collection $accounts - * @param array $types - * @param Carbon $start - * @param Carbon $end - * - * @return string - */ - private function sumInPeriodWithoutCategory(Collection $accounts, array $types, Carbon $start, Carbon $end): string - { - $query = $this->user->transactionJournals() - ->distinct() - ->transactionTypes($types) - ->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin( - 'transactions as t', function (JoinClause $join) { - $join->on('t.transaction_journal_id', '=', 'transaction_journals.id')->where('amount', '<', 0); - } - ) - ->leftJoin('category_transaction', 't.id', '=', 'category_transaction.transaction_id') - ->whereNull('category_transaction_journal.id') - ->whereNull('category_transaction.id') - ->before($end) - ->after($start); - - if ($accounts->count() > 0) { - $accountIds = $accounts->pluck('id')->toArray(); - - $query->whereIn('t.account_id', $accountIds); - } - $sum = strval($query->sum('t.amount')); - - return $sum; - - } - } diff --git a/app/Repositories/Category/CategoryRepositoryInterface.php b/app/Repositories/Category/CategoryRepositoryInterface.php index d5aaff5bc2..9e89a78f50 100644 --- a/app/Repositories/Category/CategoryRepositoryInterface.php +++ b/app/Repositories/Category/CategoryRepositoryInterface.php @@ -15,7 +15,7 @@ namespace FireflyIII\Repositories\Category; use Carbon\Carbon; use FireflyIII\Models\Category; -use Illuminate\Pagination\LengthAwarePaginator; +use FireflyIII\User; use Illuminate\Support\Collection; /** @@ -25,7 +25,6 @@ use Illuminate\Support\Collection; */ interface CategoryRepositoryInterface { - /** * @param Category $category * @@ -43,15 +42,6 @@ interface CategoryRepositoryInterface */ public function earnedInPeriod(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): string; - /** - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end - * - * @return string - */ - public function earnedInPeriodWithoutCategory(Collection $accounts, Carbon $start, Carbon $end) :string; - /** * Find a category * @@ -59,7 +49,7 @@ interface CategoryRepositoryInterface * * @return Category */ - public function find(int $categoryId) : Category; + public function find(int $categoryId): Category; /** * Find a category @@ -68,7 +58,7 @@ interface CategoryRepositoryInterface * * @return Category */ - public function findByName(string $name) : Category; + public function findByName(string $name): Category; /** * @param Category $category @@ -84,36 +74,6 @@ interface CategoryRepositoryInterface */ public function getCategories(): Collection; - /** - * @param Category $category - * @param int $page - * @param int $pageSize - * - * @return LengthAwarePaginator - */ - public function getJournals(Category $category, int $page, int $pageSize): LengthAwarePaginator; - - /** - * @param Collection $categories - * @param Collection $accounts - * @param array $types - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function journalsInPeriod(Collection $categories, Collection $accounts, array $types, Carbon $start, Carbon $end): Collection; - - /** - * @param Collection $accounts - * @param array $types - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function journalsInPeriodWithoutCategory(Collection $accounts, array $types, Carbon $start, Carbon $end) : Collection; - /** * Return most recent transaction(journal) date. * @@ -124,6 +84,49 @@ interface CategoryRepositoryInterface */ public function lastUseDate(Category $category, Collection $accounts): Carbon; + /** + * @param Collection $categories + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function periodExpenses(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array; + + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function periodExpensesNoCategory(Collection $accounts, Carbon $start, Carbon $end): array; + + /** + * @param Collection $categories + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function periodIncome(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array; + + /** + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function periodIncomeNoCategory(Collection $accounts, Carbon $start, Carbon $end): array; + + /** + * @param User $user + */ + public function setUser(User $user); + /** * @param Collection $categories * @param Collection $accounts @@ -141,7 +144,7 @@ interface CategoryRepositoryInterface * * @return string */ - public function spentInPeriodWithoutCategory(Collection $accounts, Carbon $start, Carbon $end) : string; + public function spentInPeriodWithoutCategory(Collection $accounts, Carbon $start, Carbon $end): string; /** * @param array $data diff --git a/app/Repositories/Currency/CurrencyRepository.php b/app/Repositories/Currency/CurrencyRepository.php index 3b88e9ede4..bb1444d73c 100644 --- a/app/Repositories/Currency/CurrencyRepository.php +++ b/app/Repositories/Currency/CurrencyRepository.php @@ -16,7 +16,9 @@ namespace FireflyIII\Repositories\Currency; use FireflyIII\Models\Preference; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\User; use Illuminate\Support\Collection; +use Preferences; /** * Class CurrencyRepository @@ -25,6 +27,48 @@ use Illuminate\Support\Collection; */ class CurrencyRepository implements CurrencyRepositoryInterface { + /** @var User */ + private $user; + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + + /** + * @param TransactionCurrency $currency + * + * @return bool + */ + public function canDeleteCurrency(TransactionCurrency $currency): bool + { + if ($this->countJournals($currency) > 0) { + return false; + } + + // is the only currency left + if ($this->get()->count() === 1) { + return false; + } + + // is the default currency for the user or the system + $defaultCode = Preferences::getForUser($this->user, 'currencyPreference', config('firefly.default_currency', 'EUR'))->data; + if ($currency->code === $defaultCode) { + return false; + } + + // is the default currency for the system + $defaultSystemCode = config('firefly.default_currency', 'EUR'); + if ($currency->code === $defaultSystemCode) { + return false; + } + + // can be deleted + return true; + } /** * @param TransactionCurrency $currency @@ -36,6 +80,20 @@ class CurrencyRepository implements CurrencyRepositoryInterface return $currency->transactionJournals()->count(); } + /** + * @param TransactionCurrency $currency + * + * @return bool + */ + public function destroy(TransactionCurrency $currency): bool + { + if ($this->user->hasRole('owner')) { + $currency->forceDelete(); + } + + return true; + } + /** * Find by ID * @@ -43,7 +101,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface * * @return TransactionCurrency */ - public function find(int $currencyId) : TransactionCurrency + public function find(int $currencyId): TransactionCurrency { $currency = TransactionCurrency::find($currencyId); if (is_null($currency)) { @@ -61,7 +119,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface * * @return TransactionCurrency */ - public function findByCode(string $currencyCode) : TransactionCurrency + public function findByCode(string $currencyCode): TransactionCurrency { $currency = TransactionCurrency::whereCode($currencyCode)->first(); if (is_null($currency)) { @@ -78,7 +136,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface * * @return TransactionCurrency */ - public function findByName(string $currencyName) : TransactionCurrency + public function findByName(string $currencyName): TransactionCurrency { $preferred = TransactionCurrency::whereName($currencyName)->first(); if (is_null($preferred)) { @@ -95,7 +153,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface * * @return TransactionCurrency */ - public function findBySymbol(string $currencySymbol) : TransactionCurrency + public function findBySymbol(string $currencySymbol): TransactionCurrency { $currency = TransactionCurrency::whereSymbol($currencySymbol)->first(); if (is_null($currency)) { @@ -137,9 +195,10 @@ class CurrencyRepository implements CurrencyRepositoryInterface { $currency = TransactionCurrency::create( [ - 'name' => $data['name'], - 'code' => $data['code'], - 'symbol' => $data['symbol'], + 'name' => $data['name'], + 'code' => $data['code'], + 'symbol' => $data['symbol'], + 'decimal_places' => $data['decimal_places'], ] ); @@ -154,9 +213,10 @@ class CurrencyRepository implements CurrencyRepositoryInterface */ public function update(TransactionCurrency $currency, array $data): TransactionCurrency { - $currency->code = $data['code']; - $currency->symbol = $data['symbol']; - $currency->name = $data['name']; + $currency->code = $data['code']; + $currency->symbol = $data['symbol']; + $currency->name = $data['name']; + $currency->decimal_places = $data['decimal_places']; $currency->save(); return $currency; diff --git a/app/Repositories/Currency/CurrencyRepositoryInterface.php b/app/Repositories/Currency/CurrencyRepositoryInterface.php index 4bd01fa256..63eb9bf0b5 100644 --- a/app/Repositories/Currency/CurrencyRepositoryInterface.php +++ b/app/Repositories/Currency/CurrencyRepositoryInterface.php @@ -16,6 +16,7 @@ namespace FireflyIII\Repositories\Currency; use FireflyIII\Models\Preference; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\User; use Illuminate\Support\Collection; /** @@ -25,6 +26,13 @@ use Illuminate\Support\Collection; */ interface CurrencyRepositoryInterface { + /** + * @param TransactionCurrency $currency + * + * @return bool + */ + public function canDeleteCurrency(TransactionCurrency $currency): bool; + /** * @param TransactionCurrency $currency * @@ -32,6 +40,13 @@ interface CurrencyRepositoryInterface */ public function countJournals(TransactionCurrency $currency): int; + /** + * @param TransactionCurrency $currency + * + * @return bool + */ + public function destroy(TransactionCurrency $currency): bool; + /** * Find by ID * @@ -39,7 +54,7 @@ interface CurrencyRepositoryInterface * * @return TransactionCurrency */ - public function find(int $currencyId) : TransactionCurrency; + public function find(int $currencyId): TransactionCurrency; /** * Find by currency code @@ -48,7 +63,7 @@ interface CurrencyRepositoryInterface * * @return TransactionCurrency */ - public function findByCode(string $currencyCode) : TransactionCurrency; + public function findByCode(string $currencyCode): TransactionCurrency; /** * Find by currency name @@ -57,7 +72,7 @@ interface CurrencyRepositoryInterface * * @return TransactionCurrency */ - public function findByName(string $currencyName) : TransactionCurrency; + public function findByName(string $currencyName): TransactionCurrency; /** * Find by currency symbol @@ -66,7 +81,7 @@ interface CurrencyRepositoryInterface * * @return TransactionCurrency */ - public function findBySymbol(string $currencySymbol) : TransactionCurrency; + public function findBySymbol(string $currencySymbol): TransactionCurrency; /** * @return Collection @@ -80,6 +95,11 @@ interface CurrencyRepositoryInterface */ public function getCurrencyByPreference(Preference $preference): TransactionCurrency; + /** + * @param User $user + */ + public function setUser(User $user); + /** * @param array $data * diff --git a/app/Repositories/ExportJob/ExportJobRepository.php b/app/Repositories/ExportJob/ExportJobRepository.php index 1aae76d43e..aa956f901e 100644 --- a/app/Repositories/ExportJob/ExportJobRepository.php +++ b/app/Repositories/ExportJob/ExportJobRepository.php @@ -17,6 +17,7 @@ use Carbon\Carbon; use FireflyIII\Models\ExportJob; use FireflyIII\User; use Illuminate\Support\Str; +use Storage; /** * Class ExportJobRepository @@ -29,13 +30,16 @@ class ExportJobRepository implements ExportJobRepositoryInterface private $user; /** - * ExportJobRepository constructor. + * @param ExportJob $job + * @param string $status * - * @param User $user + * @return bool */ - public function __construct(User $user) + public function changeStatus(ExportJob $job, string $status): bool { - $this->user = $user; + $job->change($status); + + return true; } /** @@ -94,10 +98,23 @@ class ExportJobRepository implements ExportJobRepositoryInterface } + /** + * @param ExportJob $job + * + * @return bool + */ + public function exists(ExportJob $job): bool + { + $disk = Storage::disk('export'); + $file = $job->key . '.zip'; + + return $disk->exists($file); + } + /** * @param string $key * - * @return ExportJob|null + * @return ExportJob */ public function findByKey(string $key): ExportJob { @@ -109,4 +126,25 @@ class ExportJobRepository implements ExportJobRepositoryInterface return $result; } + /** + * @param ExportJob $job + * + * @return string + */ + public function getContent(ExportJob $job): string + { + $disk = Storage::disk('export'); + $file = $job->key . '.zip'; + $content = $disk->get($file); + + return $content; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } } diff --git a/app/Repositories/ExportJob/ExportJobRepositoryInterface.php b/app/Repositories/ExportJob/ExportJobRepositoryInterface.php index c9d6167873..7e93663ea5 100644 --- a/app/Repositories/ExportJob/ExportJobRepositoryInterface.php +++ b/app/Repositories/ExportJob/ExportJobRepositoryInterface.php @@ -14,6 +14,7 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\ExportJob; use FireflyIII\Models\ExportJob; +use FireflyIII\User; /** * Interface ExportJobRepositoryInterface @@ -22,6 +23,14 @@ use FireflyIII\Models\ExportJob; */ interface ExportJobRepositoryInterface { + /** + * @param ExportJob $job + * @param string $status + * + * @return bool + */ + public function changeStatus(ExportJob $job, string $status): bool; + /** * @return bool */ @@ -32,11 +41,30 @@ interface ExportJobRepositoryInterface */ public function create(): ExportJob; + /** + * @param ExportJob $job + * + * @return bool + */ + public function exists(ExportJob $job): bool; + /** * @param string $key * - * @return ExportJob|null + * @return ExportJob */ public function findByKey(string $key): ExportJob; + /** + * @param ExportJob $job + * + * @return string + */ + public function getContent(ExportJob $job): string; + + /** + * @param User $user + */ + public function setUser(User $user); + } diff --git a/app/Repositories/ImportJob/ImportJobRepository.php b/app/Repositories/ImportJob/ImportJobRepository.php index d7f4ac30cf..0eec759e5b 100644 --- a/app/Repositories/ImportJob/ImportJobRepository.php +++ b/app/Repositories/ImportJob/ImportJobRepository.php @@ -28,16 +28,6 @@ class ImportJobRepository implements ImportJobRepositoryInterface /** @var User */ private $user; - /** - * ExportJobRepository constructor. - * - * @param User $user - */ - public function __construct(User $user) - { - $this->user = $user; - } - /** * @param string $fileType * @@ -95,4 +85,12 @@ class ImportJobRepository implements ImportJobRepositoryInterface return $result; } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } } diff --git a/app/Repositories/ImportJob/ImportJobRepositoryInterface.php b/app/Repositories/ImportJob/ImportJobRepositoryInterface.php index a17d9ca62c..5bb2149fbe 100644 --- a/app/Repositories/ImportJob/ImportJobRepositoryInterface.php +++ b/app/Repositories/ImportJob/ImportJobRepositoryInterface.php @@ -14,6 +14,7 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\ImportJob; use FireflyIII\Models\ImportJob; +use FireflyIII\User; /** * Interface ImportJobRepositoryInterface @@ -35,4 +36,9 @@ interface ImportJobRepositoryInterface * @return ImportJob */ public function findByKey(string $key): ImportJob; + + /** + * @param User $user + */ + public function setUser(User $user); } diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 6399d98f0d..12ce898ff1 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -26,7 +26,9 @@ use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\User; use Illuminate\Support\Collection; +use Illuminate\Support\MessageBag; use Log; +use Preferences; /** * Class JournalRepository @@ -42,15 +44,53 @@ class JournalRepository implements JournalRepositoryInterface private $validMetaFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date', 'internal_reference', 'notes']; /** - * JournalRepository constructor. - * * @param User $user */ - public function __construct(User $user) + public function setUser(User $user) { $this->user = $user; } + /** + * @param TransactionJournal $journal + * @param TransactionType $type + * @param Account $source + * @param Account $destination + * + * @return MessageBag + */ + public function convert(TransactionJournal $journal, TransactionType $type, Account $source, Account $destination): MessageBag + { + // default message bag that shows errors for everything. + $messages = new MessageBag; + $messages->add('source_account_revenue', trans('firefly.invalid_convert_selection')); + $messages->add('destination_account_asset', trans('firefly.invalid_convert_selection')); + $messages->add('destination_account_expense', trans('firefly.invalid_convert_selection')); + $messages->add('source_account_asset', trans('firefly.invalid_convert_selection')); + + if ($source->id === $destination->id || is_null($source->id) || is_null($destination->id)) { + return $messages; + } + + $sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first(); + $destinationTransaction = $journal->transactions()->where('amount', '>', 0)->first(); + $sourceTransaction->account_id = $source->id; + $sourceTransaction->save(); + $destinationTransaction->account_id = $destination->id; + $destinationTransaction->save(); + $journal->transaction_type_id = $type->id; + $journal->save(); + + // if journal is a transfer now, remove budget: + if ($type->type === TransactionType::TRANSFER) { + $journal->budgets()->detach(); + } + + Preferences::mark(); + + return new MessageBag; + } + /** * @param TransactionJournal $journal * @@ -68,7 +108,7 @@ class JournalRepository implements JournalRepositoryInterface * * @return TransactionJournal */ - public function find(int $journalId) : TransactionJournal + public function find(int $journalId): TransactionJournal { $journal = $this->user->transactionJournals()->where('id', $journalId)->first(); if (is_null($journal)) { @@ -95,6 +135,13 @@ class JournalRepository implements JournalRepositoryInterface return $entry; } + /** + * @return Collection + */ + public function getTransactionTypes(): Collection + { + return TransactionType::orderBy('type', 'ASC')->get(); + } /** * @param array $data @@ -167,40 +214,6 @@ class JournalRepository implements JournalRepositoryInterface } - /** - * Store journal only, uncompleted, with attachments if necessary. - * - * @param array $data - * - * @return TransactionJournal - */ - public function storeJournal(array $data): TransactionJournal - { - // find transaction type. - $transactionType = TransactionType::where('type', ucfirst($data['what']))->first(); - - // store actual journal. - $journal = new TransactionJournal( - [ - 'user_id' => $this->user->id, - 'transaction_type_id' => $transactionType->id, - 'transaction_currency_id' => $data['amount_currency_id_amount'], - 'description' => $data['description'], - 'completed' => 0, - 'date' => $data['date'], - ] - ); - - $result = $journal->save(); - if ($result) { - return $journal; - } - - return new TransactionJournal(); - - - } - /** * @param TransactionJournal $journal * @param array $data @@ -361,6 +374,7 @@ class JournalRepository implements JournalRepositoryInterface 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); } } @@ -422,7 +436,7 @@ class JournalRepository implements JournalRepositoryInterface */ private function storeBudgetWithJournal(TransactionJournal $journal, int $budgetId) { - if (intval($budgetId) > 0 && $journal->transactionType->type !== TransactionType::TRANSFER) { + if (intval($budgetId) > 0 && $journal->transactionType->type === TransactionType::WITHDRAWAL) { /** @var \FireflyIII\Models\Budget $budget */ $budget = Budget::find($budgetId); $journal->budgets()->save($budget); @@ -473,20 +487,28 @@ class JournalRepository implements JournalRepositoryInterface */ 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, ]; } - $sourceType = AccountType::where('type', 'Cash account')->first(); + + 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] ); @@ -581,8 +603,11 @@ class JournalRepository implements JournalRepositoryInterface */ 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( @@ -594,12 +619,15 @@ class JournalRepository implements JournalRepositoryInterface ] ); + Log::debug(sprintf('destination account name is "%s", account is %d', $data['destination_account_name'], $destinationAccount->id)); + return [ 'source' => $sourceAccount, 'destination' => $destinationAccount, ]; } - $destinationType = AccountType::where('type', 'Cash account')->first(); + 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] ); @@ -697,6 +725,7 @@ class JournalRepository implements JournalRepositoryInterface // 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); } diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php index 5db74eb818..f1f5438ad4 100644 --- a/app/Repositories/Journal/JournalRepositoryInterface.php +++ b/app/Repositories/Journal/JournalRepositoryInterface.php @@ -13,7 +13,12 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Journal; +use FireflyIII\Models\Account; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use FireflyIII\User; +use Illuminate\Support\Collection; +use Illuminate\Support\MessageBag; /** * Interface JournalRepositoryInterface @@ -22,6 +27,15 @@ use FireflyIII\Models\TransactionJournal; */ interface JournalRepositoryInterface { + /** + * @param TransactionJournal $journal + * @param TransactionType $type + * @param Account $source + * @param Account $destination + * + * @return MessageBag + */ + public function convert(TransactionJournal $journal, TransactionType $type, Account $source, Account $destination): MessageBag; /** * Deletes a journal. @@ -39,7 +53,7 @@ interface JournalRepositoryInterface * * @return TransactionJournal */ - public function find(int $journalId) : TransactionJournal; + public function find(int $journalId): TransactionJournal; /** * Get users very first transaction journal @@ -48,6 +62,15 @@ interface JournalRepositoryInterface */ public function first(): TransactionJournal; + /** + * @return Collection + */ + public function getTransactionTypes(): Collection; + + /** + * @param User $user + */ + public function setUser(User $user); /** * @param array $data @@ -56,15 +79,6 @@ interface JournalRepositoryInterface */ public function store(array $data): TransactionJournal; - /** - * Store journal only, uncompleted, with attachments if necessary. - * - * @param array $data - * - * @return TransactionJournal - */ - public function storeJournal(array $data): TransactionJournal; - /** * @param TransactionJournal $journal * @param array $data diff --git a/app/Repositories/Journal/JournalTasker.php b/app/Repositories/Journal/JournalTasker.php index a51f356a28..c0424242c8 100644 --- a/app/Repositories/Journal/JournalTasker.php +++ b/app/Repositories/Journal/JournalTasker.php @@ -13,15 +13,15 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Journal; -use Carbon\Carbon; use Crypt; use DB; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\PiggyBankEvent; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\JoinClause; -use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; /** @@ -35,82 +35,6 @@ class JournalTasker implements JournalTaskerInterface /** @var User */ private $user; - /** - * JournalRepository constructor. - * - * @param User $user - */ - public function __construct(User $user) - { - $this->user = $user; - } - - /** - * Returns a page of a specific type(s) of journal. - * - * @param array $types - * @param int $page - * @param int $pageSize - * - * @return LengthAwarePaginator - */ - public function getJournals(array $types, int $page, int $pageSize = 50): LengthAwarePaginator - { - $offset = ($page - 1) * $pageSize; - $query = $this->user->transactionJournals()->expanded()->sortCorrectly(); - $query->where('transaction_journals.completed', 1); - if (count($types) > 0) { - $query->transactionTypes($types); - } - $count = $this->user->transactionJournals()->transactionTypes($types)->count(); - $set = $query->take($pageSize)->offset($offset)->get(TransactionJournal::queryFields()); - $journals = new LengthAwarePaginator($set, $count, $pageSize, $page); - - return $journals; - } - - /** - * Returns a collection of ALL journals, given a specific account and a date range. - * - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function getJournalsInRange(Collection $accounts, Carbon $start, Carbon $end): Collection - { - $query = $this->user->transactionJournals()->expanded()->sortCorrectly(); - $query->where('transaction_journals.completed', 1); - $query->before($end); - $query->after($start); - - if ($accounts->count() > 0) { - $ids = $accounts->pluck('id')->toArray(); - // join source and destination: - $query->leftJoin( - 'transactions as source', function (JoinClause $join) { - $join->on('source.transaction_journal_id', '=', 'transaction_journals.id')->where('source.amount', '<', 0); - } - ); - $query->leftJoin( - 'transactions as destination', function (JoinClause $join) { - $join->on('destination.transaction_journal_id', '=', 'transaction_journals.id')->where('destination.amount', '>', 0); - } - ); - - $query->where( - function (Builder $q) use ($ids) { - $q->whereIn('destination.account_id', $ids); - $q->orWhereIn('source.account_id', $ids); - } - ); - } - - $set = $query->get(TransactionJournal::queryFields()); - - return $set; - } /** * @param TransactionJournal $journal @@ -141,34 +65,6 @@ class JournalTasker implements JournalTaskerInterface public function getTransactionsOverview(TransactionJournal $journal): array { // get all transaction data + the opposite site in one list. - /** - * select - * - * source.id, - * source.account_id, - * source_accounts.name as account_name, - * source_accounts.encrypted as account_encrypted, - * source.amount, - * source.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 - * - * - * from transactions as source - * - * left join transactions as destination ON source.transaction_journal_id = - * destination.transaction_journal_id AND source.amount = destination.amount * -1 AND source.identifier = destination.identifier - * -- left join source account name: - * left join accounts as source_accounts ON source.account_id = source_accounts.id - * left join accounts as destination_accounts ON destination.account_id = destination_accounts.id - * - * where source.transaction_journal_id = 6600 - * and source.amount < 0 - * and source.deleted_at is null - */ $set = $journal ->transactions()// "source" ->leftJoin( @@ -182,7 +78,9 @@ class JournalTasker implements JournalTaskerInterface ) ->with(['budgets', 'categories']) ->leftJoin('accounts as source_accounts', 'transactions.account_id', '=', 'source_accounts.id') + ->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') ->where('transactions.amount', '<', 0) ->whereNull('transactions.deleted_at') ->get( @@ -191,12 +89,14 @@ class JournalTasker implements JournalTaskerInterface 'transactions.account_id', 'source_accounts.name as account_name', 'source_accounts.encrypted as account_encrypted', + 'source_account_types.type as account_type', 'transactions.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', ] ); @@ -204,22 +104,23 @@ class JournalTasker implements JournalTaskerInterface /** @var Transaction $entry */ foreach ($set as $entry) { - $sourceBalance = $this->getBalance($entry->id); - $destinationBalance = $this->getBalance($entry->destination_id); + $sourceBalance = $this->getBalance(intval($entry->id)); + $destinationBalance = $this->getBalance(intval($entry->destination_id)); $budget = $entry->budgets->first(); $category = $entry->categories->first(); $transaction = [ - 'source_id' => $entry->id, - 'source_amount' => $entry->amount, - + 'source_id' => $entry->id, + 'source_amount' => $entry->amount, 'description' => $entry->description, 'source_account_id' => $entry->account_id, 'source_account_name' => intval($entry->account_encrypted) === 1 ? Crypt::decrypt($entry->account_name) : $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' => intval($entry->destination_account_encrypted) === 1 ? Crypt::decrypt($entry->destination_account_name) : $entry->destination_account_name, 'destination_account_before' => $destinationBalance, @@ -227,6 +128,13 @@ class JournalTasker implements JournalTaskerInterface 'budget_id' => is_null($budget) ? 0 : $budget->id, 'category' => is_null($category) ? '' : $category->name, ]; + if ($entry->destination_account_type === AccountType::CASH) { + $transaction['destination_account_name'] = ''; + } + + if ($entry->account_type === AccountType::CASH) { + $transaction['source_account_name'] = ''; + } $transactions[] = $transaction; @@ -235,6 +143,14 @@ class JournalTasker implements JournalTaskerInterface return $transactions; } + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + /** * Collect the balance of an account before the given transaction has hit. This is tricky, because * the balance does not depend on the transaction itself but the journal it's part of. And of course @@ -254,39 +170,37 @@ class JournalTasker implements JournalTaskerInterface $identifier = intval($transaction->identifier); // go! - $sum - = Transaction - ::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('account_id', $transaction->account_id) - ->whereNull('transactions.deleted_at') - ->whereNull('transaction_journals.deleted_at') - ->where('transactions.id', '!=', $transactionId) - ->where( - function (Builder $q1) use ($date, $order, $journalId, $identifier) { - $q1->where('transaction_journals.date', '<', $date); // date - $q1->orWhere( - function (Builder $q2) use ($date, $order) { // function 1 - $q2->where('transaction_journals.date', $date); - $q2->where('transaction_journals.order', '>', $order); - } - ); - $q1->orWhere( - function (Builder $q3) use ($date, $order, $journalId) { // function 2 - $q3->where('transaction_journals.date', $date); - $q3->where('transaction_journals.order', $order); - $q3->where('transaction_journals.id', '<', $journalId); - } - ); - $q1->orWhere( - function (Builder $q4) use ($date, $order, $journalId, $identifier) { // function 3 - $q4->where('transaction_journals.date', $date); - $q4->where('transaction_journals.order', $order); - $q4->where('transaction_journals.id', $journalId); - $q4->where('transactions.identifier', '>', $identifier); - } - ); - } - )->sum('transactions.amount'); + $sum = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('account_id', $transaction->account_id) + ->whereNull('transactions.deleted_at') + ->whereNull('transaction_journals.deleted_at') + ->where('transactions.id', '!=', $transactionId) + ->where( + function (Builder $q1) use ($date, $order, $journalId, $identifier) { + $q1->where('transaction_journals.date', '<', $date); // date + $q1->orWhere( + function (Builder $q2) use ($date, $order) { // function 1 + $q2->where('transaction_journals.date', $date); + $q2->where('transaction_journals.order', '>', $order); + } + ); + $q1->orWhere( + function (Builder $q3) use ($date, $order, $journalId) { // function 2 + $q3->where('transaction_journals.date', $date); + $q3->where('transaction_journals.order', $order); + $q3->where('transaction_journals.id', '<', $journalId); + } + ); + $q1->orWhere( + function (Builder $q4) use ($date, $order, $journalId, $identifier) { // function 3 + $q4->where('transaction_journals.date', $date); + $q4->where('transaction_journals.order', $order); + $q4->where('transaction_journals.id', $journalId); + $q4->where('transactions.identifier', '>', $identifier); + } + ); + } + )->sum('transactions.amount'); return strval($sum); } diff --git a/app/Repositories/Journal/JournalTaskerInterface.php b/app/Repositories/Journal/JournalTaskerInterface.php index 89ae1c8289..1273f69c3a 100644 --- a/app/Repositories/Journal/JournalTaskerInterface.php +++ b/app/Repositories/Journal/JournalTaskerInterface.php @@ -14,9 +14,8 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Journal; -use Carbon\Carbon; use FireflyIII\Models\TransactionJournal; -use Illuminate\Pagination\LengthAwarePaginator; +use FireflyIII\User; use Illuminate\Support\Collection; /** @@ -26,28 +25,6 @@ use Illuminate\Support\Collection; */ interface JournalTaskerInterface { - /** - * Returns a page of a specific type(s) of journal. - * - * @param array $types - * @param int $page - * @param int $pageSize - * - * @return LengthAwarePaginator - */ - public function getJournals(array $types, int $page, int $pageSize = 50): LengthAwarePaginator; - - /** - * Returns a collection of ALL journals, given a specific account and a date range. - * - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function getJournalsInRange(Collection $accounts, Carbon $start, Carbon $end): Collection; - /** * @param TransactionJournal $journal * @@ -64,4 +41,9 @@ interface JournalTaskerInterface * @return array */ public function getTransactionsOverview(TransactionJournal $journal): array; + + /** + * @param User $user + */ + public function setUser(User $user); } diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index 6eef1e358e..1f14467a5b 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -32,16 +32,6 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface /** @var User */ private $user; - /** - * PiggyBankRepository constructor. - * - * @param User $user - */ - public function __construct(User $user) - { - $this->user = $user; - } - /** * @param PiggyBank $piggyBank * @param string $amount @@ -117,7 +107,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface * * @return Collection */ - public function getPiggyBanksWithAmount() : Collection + public function getPiggyBanksWithAmount(): Collection { $set = $this->getPiggyBanks(); foreach ($set as $piggy) { @@ -136,8 +126,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface public function reset(): bool { // split query to make it work in sqlite: - $set = PiggyBank:: - leftJoin('accounts', 'accounts.id', '=', 'piggy_banks.id') + $set = PiggyBank::leftJoin('accounts', 'accounts.id', '=', 'piggy_banks.id') ->where('accounts.user_id', $this->user->id)->get(['piggy_banks.*']); foreach ($set as $e) { $e->order = 0; @@ -168,6 +157,14 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface return true; } + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + /** * @param array $data * diff --git a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php index 22eb0a4e9a..a2da326d86 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php +++ b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php @@ -15,6 +15,7 @@ namespace FireflyIII\Repositories\PiggyBank; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBankEvent; +use FireflyIII\User; use Illuminate\Support\Collection; /** @@ -58,7 +59,7 @@ interface PiggyBankRepositoryInterface * * @return Collection */ - public function getEvents(PiggyBank $piggyBank) : Collection; + public function getEvents(PiggyBank $piggyBank): Collection; /** * Highest order of all piggy banks. @@ -72,14 +73,14 @@ interface PiggyBankRepositoryInterface * * @return Collection */ - public function getPiggyBanks() : Collection; + public function getPiggyBanks(): Collection; /** * Also add amount in name. * * @return Collection */ - public function getPiggyBanksWithAmount() : Collection; + public function getPiggyBanksWithAmount(): Collection; /** * Set all piggy banks to order 0. @@ -98,6 +99,10 @@ interface PiggyBankRepositoryInterface */ public function setOrder(int $piggyBankId, int $order): bool; + /** + * @param User $user + */ + public function setUser(User $user); /** * Store new piggy bank. diff --git a/app/Repositories/Rule/RuleRepository.php b/app/Repositories/Rule/RuleRepository.php index 4b7ce00433..7e0487be63 100644 --- a/app/Repositories/Rule/RuleRepository.php +++ b/app/Repositories/Rule/RuleRepository.php @@ -30,16 +30,6 @@ class RuleRepository implements RuleRepositoryInterface /** @var User */ private $user; - /** - * BillRepository constructor. - * - * @param User $user - */ - public function __construct(User $user) - { - $this->user = $user; - } - /** * @return int */ @@ -218,6 +208,14 @@ class RuleRepository implements RuleRepositoryInterface } + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + /** * @param array $data * diff --git a/app/Repositories/Rule/RuleRepositoryInterface.php b/app/Repositories/Rule/RuleRepositoryInterface.php index 518417d94a..6ca1456214 100644 --- a/app/Repositories/Rule/RuleRepositoryInterface.php +++ b/app/Repositories/Rule/RuleRepositoryInterface.php @@ -17,6 +17,7 @@ use FireflyIII\Models\Rule; use FireflyIII\Models\RuleAction; use FireflyIII\Models\RuleGroup; use FireflyIII\Models\RuleTrigger; +use FireflyIII\User; /** * Interface RuleRepositoryInterface @@ -25,7 +26,6 @@ use FireflyIII\Models\RuleTrigger; */ interface RuleRepositoryInterface { - /** * @return int */ @@ -94,6 +94,11 @@ interface RuleRepositoryInterface */ public function resetRulesInGroupOrder(RuleGroup $ruleGroup): bool; + /** + * @param User $user + */ + public function setUser(User $user); + /** * @param array $data * diff --git a/app/Repositories/RuleGroup/RuleGroupRepository.php b/app/Repositories/RuleGroup/RuleGroupRepository.php index 247078221e..e413987334 100644 --- a/app/Repositories/RuleGroup/RuleGroupRepository.php +++ b/app/Repositories/RuleGroup/RuleGroupRepository.php @@ -31,11 +31,9 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface private $user; /** - * BillRepository constructor. - * * @param User $user */ - public function __construct(User $user) + public function setUser(User $user) { $this->user = $user; } @@ -49,8 +47,8 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface } /** - * @param RuleGroup $ruleGroup - * @param RuleGroup $moveTo + * @param RuleGroup $ruleGroup + * @param RuleGroup|null $moveTo * * @return bool */ diff --git a/app/Repositories/RuleGroup/RuleGroupRepositoryInterface.php b/app/Repositories/RuleGroup/RuleGroupRepositoryInterface.php index a321b04e98..c365fca704 100644 --- a/app/Repositories/RuleGroup/RuleGroupRepositoryInterface.php +++ b/app/Repositories/RuleGroup/RuleGroupRepositoryInterface.php @@ -26,7 +26,6 @@ use Illuminate\Support\Collection; interface RuleGroupRepositoryInterface { - /** * * @@ -35,8 +34,8 @@ interface RuleGroupRepositoryInterface public function count(): int; /** - * @param RuleGroup $ruleGroup - * @param RuleGroup $moveTo + * @param RuleGroup $ruleGroup + * @param RuleGroup|null $moveTo * * @return bool */ @@ -92,6 +91,11 @@ interface RuleGroupRepositoryInterface */ public function resetRulesInGroupOrder(RuleGroup $ruleGroup): bool; + /** + * @param User $user + */ + public function setUser(User $user); + /** * @param array $data * diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index 6c29921d88..9f91816252 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -14,11 +14,14 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Tag; +use Carbon\Carbon; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\User; use Illuminate\Support\Collection; +use Log; /** * Class TagRepository @@ -32,11 +35,9 @@ class TagRepository implements TagRepositoryInterface private $user; /** - * TagRepository constructor. - * * @param User $user */ - public function __construct(User $user) + public function setUser(User $user) { $this->user = $user; } @@ -54,11 +55,14 @@ class TagRepository implements TagRepositoryInterface * Already connected: */ if ($journal->tags()->find($tag->id)) { + Log::error(sprintf('Cannot find tag #%d', $tag->id)); + return false; } switch ($tag->tagMode) { case 'nothing': + Log::debug(sprintf('Tag #%d connected', $tag->id)); $journal->tags()->save($tag); $journal->save(); @@ -89,7 +93,7 @@ class TagRepository implements TagRepositoryInterface * * @return Tag */ - public function find(int $tagId) : Tag + public function find(int $tagId): Tag { $tag = $this->user->tags()->find($tagId); if (is_null($tag)) { @@ -104,7 +108,7 @@ class TagRepository implements TagRepositoryInterface * * @return Tag */ - public function findByTag(string $tag) : Tag + public function findByTag(string $tag): Tag { $tags = $this->user->tags()->get(); /** @var Tag $tag */ @@ -117,6 +121,21 @@ class TagRepository implements TagRepositoryInterface return new Tag; } + /** + * @param Tag $tag + * + * @return Carbon + */ + public function firstUseDate(Tag $tag): Carbon + { + $journal = $tag->transactionJournals()->orderBy('date', 'ASC')->first(); + if (!is_null($journal)) { + return $journal->date; + } + + return new Carbon; + } + /** * @return Collection */ @@ -136,19 +155,16 @@ class TagRepository implements TagRepositoryInterface /** * @param Tag $tag * - * @return Collection + * @return Carbon */ - public function getJournals(Tag $tag) : Collection + public function lastUseDate(Tag $tag): Carbon { - /** @var Collection $journals */ - $journals = $tag - ->transactionJournals() - ->sortCorrectly() - ->expanded() - ->groupBy(['tag_transaction_journal.tag_id', 'tag_transaction_journal.transaction_journal_id']) - ->get(TransactionJournal::queryFields()); + $journal = $tag->transactionJournals()->orderBy('date', 'DESC')->first(); + if (!is_null($journal)) { + return $journal->date; + } - return $journals; + return new Carbon; } /** @@ -202,22 +218,23 @@ class TagRepository implements TagRepositoryInterface */ protected function connectAdvancePayment(TransactionJournal $journal, Tag $tag): bool { - /** @var TransactionType $transfer */ - $transfer = TransactionType::whereType(TransactionType::TRANSFER)->first(); - /** @var TransactionType $withdrawal */ - $withdrawal = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); - /** @var TransactionType $deposit */ - $deposit = TransactionType::whereType(TransactionType::DEPOSIT)->first(); + $type = $journal->transactionType->type; + $withdrawals = $tag->transactionJournals() + ->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id') + ->where('transaction_types.type', TransactionType::WITHDRAWAL)->count(); + $deposits = $tag->transactionJournals() + ->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id') + ->where('transaction_types.type', TransactionType::DEPOSIT)->count(); - $withdrawals = $tag->transactionJournals()->where('transaction_type_id', $withdrawal->id)->count(); - $deposits = $tag->transactionJournals()->where('transaction_type_id', $deposit->id)->count(); + if ($type === TransactionType::TRANSFER) { // advance payments cannot accept transfers: + Log::error(sprintf('Journal #%d is a transfer and cannot connect to tag #%d', $journal->id, $tag->id)); - if ($journal->transaction_type_id == $transfer->id) { // advance payments cannot accept transfers: return false; } // the first transaction to be attached to this tag is attached just like that: if ($withdrawals < 1 && $deposits < 1) { + Log::debug(sprintf('Tag #%d has 0 withdrawals and 0 deposits so its fine.', $tag->id)); $journal->tags()->save($tag); $journal->save(); @@ -225,12 +242,16 @@ class TagRepository implements TagRepositoryInterface } // if withdrawal and already has a withdrawal, return false: - if ($journal->transaction_type_id == $withdrawal->id && $withdrawals == 1) { + if ($type === TransactionType::WITHDRAWAL && $withdrawals > 0) { + Log::error(sprintf('Journal #%d is a withdrawal but tag already has %d withdrawal(s).', $journal->id, $withdrawals)); + return false; } // if already has transaction journals, must match ALL asset account id's: if ($deposits > 0 || $withdrawals == 1) { + Log::debug('Need to match all asset accounts.'); + return $this->matchAll($journal, $tag); } @@ -247,28 +268,39 @@ class TagRepository implements TagRepositoryInterface */ protected function connectBalancingAct(TransactionJournal $journal, Tag $tag): bool { - /** @var TransactionType $withdrawal */ - $withdrawal = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); - $withdrawals = $tag->transactionJournals()->where('transaction_type_id', $withdrawal->id)->count(); - /** @var TransactionType $transfer */ - $transfer = TransactionType::whereType(TransactionType::TRANSFER)->first(); - $transfers = $tag->transactionJournals()->where('transaction_type_id', $transfer->id)->count(); + $type = $journal->transactionType->type; + $withdrawals = $tag->transactionJournals() + ->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id') + ->where('transaction_types.type', TransactionType::WITHDRAWAL)->count(); + $transfers = $tag->transactionJournals() + ->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id') + ->where('transaction_types.type', TransactionType::TRANSFER)->count(); + Log::debug(sprintf('Journal #%d is a %s', $journal->id, $type)); + // only if this is the only withdrawal. - if ($journal->transaction_type_id == $withdrawal->id && $withdrawals < 1) { + if ($type === TransactionType::WITHDRAWAL && $withdrawals < 1) { + Log::debug('Will connect this journal because it is the only withdrawal in this tag.'); $journal->tags()->save($tag); $journal->save(); return true; } // and only if this is the only transfer - if ($journal->transaction_type_id == $transfer->id && $transfers < 1) { + if ($type === TransactionType::TRANSFER && $transfers < 1) { + Log::debug('Will connect this journal because it is the only transfer in this tag.'); $journal->tags()->save($tag); $journal->save(); return true; } + Log::error( + sprintf( + 'Tag #%d has %d withdrawals and %d transfers and cannot contain %s #%d', + $tag->id, $withdrawals, $transfers, $type, $journal->id + ) + ); // ignore expense return false; @@ -285,28 +317,64 @@ class TagRepository implements TagRepositoryInterface * * @return bool */ - protected function matchAll(TransactionJournal $journal, Tag $tag): bool + private function matchAll(TransactionJournal $journal, Tag $tag): bool { - $checkSources = join(',', TransactionJournal::sourceAccountList($journal)->pluck('id')->toArray()); - $checkDestinations = join(',', TransactionJournal::destinationAccountList($journal)->pluck('id')->toArray()); + $journalSources = join(',', array_unique(TransactionJournal::sourceAccountList($journal)->pluck('id')->toArray())); + $journalDestinations = join(',', array_unique(TransactionJournal::destinationAccountList($journal)->pluck('id')->toArray())); + $match = true; + $journals = $tag->transactionJournals()->get(['transaction_journals.*']); - $match = true; - /** @var TransactionJournal $check */ - foreach ($tag->transactionjournals as $check) { + Log::debug(sprintf('Tag #%d has %d journals to verify:', $tag->id, $journals->count())); + + /** @var TransactionJournal $existing */ + foreach ($journals as $existing) { + Log::debug(sprintf('Now existingcomparing new journal #%d to existing journal #%d', $journal->id, $existing->id)); // $checkAccount is the source_account for a withdrawal // $checkAccount is the destination_account for a deposit - $thisSources = join(',', TransactionJournal::sourceAccountList($check)->pluck('id')->toArray()); - $thisDestinations = join(',', TransactionJournal::destinationAccountList($check)->pluck('id')->toArray()); + $existingSources = join(',', array_unique(TransactionJournal::sourceAccountList($existing)->pluck('id')->toArray())); + $existingDestinations = join(',', array_unique(TransactionJournal::destinationAccountList($existing)->pluck('id')->toArray())); + + if ($existing->isWithdrawal() && $existingSources !== $journalDestinations) { + /* + * There can only be one withdrawal. And the source account(s) of the withdrawal + * must be the same as the destination of the deposit. Because any transaction that arrives + * here ($journal) must be a deposit. + */ + Log::debug(sprintf('Existing journal #%d is a withdrawal.', $existing->id)); + Log::debug(sprintf('New journal #%d must have these destination accounts: %s', $journal->id, $existingSources)); + Log::debug(sprintf('New journal #%d actually these destination accounts: %s', $journal->id, $journalDestinations)); + Log::debug('So match is FALSE'); - if ($check->isWithdrawal() && $thisSources !== $checkSources) { $match = false; } - if ($check->isDeposit() && $thisDestinations !== $checkDestinations) { + if ($existing->isDeposit() && $journal->isDeposit() && $existingDestinations !== $journalDestinations) { + /* + * There can be multiple deposits. + * They must have the destination the same as the other deposits. + */ + Log::debug(sprintf('Existing journal #%d is a deposit.', $existing->id)); + Log::debug(sprintf('Journal #%d must have these destination accounts: %s', $journal->id, $existingDestinations)); + Log::debug(sprintf('Journal #%d actually these destination accounts: %s', $journal->id, $journalDestinations)); + Log::debug('So match is FALSE'); + + $match = false; + } + + if ($existing->isDeposit() && $journal->isWithdrawal() && $existingDestinations !== $journalSources) { + /* + * There can be one new withdrawal only. It must have the same source as the existing has destination. + */ + Log::debug(sprintf('Existing journal #%d is a deposit.', $existing->id)); + Log::debug(sprintf('Journal #%d must have these source accounts: %s', $journal->id, $existingDestinations)); + Log::debug(sprintf('Journal #%d actually these source accounts: %s', $journal->id, $journalSources)); + Log::debug('So match is FALSE'); + $match = false; } } if ($match) { + Log::debug(sprintf('Match is true, connect journal #%d with tag #%d.', $journal->id, $tag->id)); $journal->tags()->save($tag); $journal->save(); @@ -315,4 +383,42 @@ class TagRepository implements TagRepositoryInterface return false; } + + /** + * @param Tag $tag + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function earnedInPeriod(Tag $tag, Carbon $start, Carbon $end): string + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAllAssetAccounts()->setTag($tag); + $set = $collector->getJournals(); + $sum = strval($set->sum('transaction_amount')); + + return $sum; + } + + /** + * @param Tag $tag + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function spentInPeriod(Tag $tag, Carbon $start, Carbon $end): string + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAllAssetAccounts()->setTag($tag); + $set = $collector->getJournals(); + $sum = strval($set->sum('transaction_amount')); + + return $sum; + } } diff --git a/app/Repositories/Tag/TagRepositoryInterface.php b/app/Repositories/Tag/TagRepositoryInterface.php index b1676b2f03..b360e7d436 100644 --- a/app/Repositories/Tag/TagRepositoryInterface.php +++ b/app/Repositories/Tag/TagRepositoryInterface.php @@ -13,8 +13,10 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Tag; +use Carbon\Carbon; use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionJournal; +use FireflyIII\User; use Illuminate\Support\Collection; @@ -44,19 +46,35 @@ interface TagRepositoryInterface */ public function destroy(Tag $tag): bool; + /** + * @param Tag $tag + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function earnedInPeriod(Tag $tag, Carbon $start, Carbon $end): string; + /** * @param int $tagId * * @return Tag */ - public function find(int $tagId) : Tag; + public function find(int $tagId): Tag; /** * @param string $tag * * @return Tag */ - public function findByTag(string $tag) : Tag; + public function findByTag(string $tag): Tag; + + /** + * @param Tag $tag + * + * @return Carbon + */ + public function firstUseDate(Tag $tag): Carbon; /** * This method returns all the user's tags. @@ -68,9 +86,23 @@ interface TagRepositoryInterface /** * @param Tag $tag * - * @return Collection + * @return Carbon */ - public function getJournals(Tag $tag) : Collection; + public function lastUseDate(Tag $tag): Carbon; + + /** + * @param User $user + */ + public function setUser(User $user); + + /** + * @param Tag $tag + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function spentInPeriod(Tag $tag, Carbon $start, Carbon $end): string; /** * This method stores a tag. diff --git a/app/Repositories/User/UserRepository.php b/app/Repositories/User/UserRepository.php index 97642b2ea9..ec2a6c7442 100644 --- a/app/Repositories/User/UserRepository.php +++ b/app/Repositories/User/UserRepository.php @@ -18,6 +18,7 @@ use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\Role; use FireflyIII\User; use Illuminate\Support\Collection; +use Log; use Preferences; /** @@ -51,6 +52,20 @@ class UserRepository implements UserRepositoryInterface return true; } + /** + * @param User $user + * @param string $password + * + * @return bool + */ + public function changePassword(User $user, string $password): bool + { + $user->password = bcrypt($password); + $user->save(); + + return true; + } + /** * @return int */ @@ -59,6 +74,19 @@ class UserRepository implements UserRepositoryInterface return $this->all()->count(); } + /** + * @param User $user + * + * @return bool + */ + public function destroy(User $user): bool + { + Log::debug(sprintf('Calling delete() on user %d', $user->id)); + $user->delete(); + + return true; + } + /** * @param int $userId * @@ -93,14 +121,6 @@ class UserRepository implements UserRepositoryInterface $return['has_2fa'] = true; } - // is user activated? - $confirmAccount = env('MUST_CONFIRM_ACCOUNT', false); - $isConfirmed = Preferences::getForUser($user, 'user_confirmed', false)->data; - $return['is_activated'] = true; - if ($isConfirmed === false && $confirmAccount === true) { - $return['is_activated'] = false; - } - $return['is_admin'] = $user->hasRole('owner'); $return['blocked'] = intval($user->blocked) === 1; $return['blocked_code'] = $user->blocked_code; @@ -112,12 +132,11 @@ class UserRepository implements UserRepositoryInterface $return['bills'] = $user->bills()->count(); $return['categories'] = $user->categories()->count(); $return['budgets'] = $user->budgets()->count(); - $return['budgets_with_limits'] = BudgetLimit - ::distinct() - ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') - ->where('amount', '>', 0) - ->whereNull('budgets.deleted_at') - ->where('budgets.user_id', $user->id)->get(['budget_limits.budget_id'])->count(); + $return['budgets_with_limits'] = BudgetLimit::distinct() + ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') + ->where('amount', '>', 0) + ->whereNull('budgets.deleted_at') + ->where('budgets.user_id', $user->id)->get(['budget_limits.budget_id'])->count(); $return['export_jobs'] = $user->exportJobs()->count(); $return['export_jobs_success'] = $user->exportJobs()->where('status', 'export_downloaded')->count(); $return['import_jobs'] = $user->exportJobs()->count(); diff --git a/app/Repositories/User/UserRepositoryInterface.php b/app/Repositories/User/UserRepositoryInterface.php index 1855fc1da1..9e2bb096b3 100644 --- a/app/Repositories/User/UserRepositoryInterface.php +++ b/app/Repositories/User/UserRepositoryInterface.php @@ -24,6 +24,7 @@ use Illuminate\Support\Collection; */ interface UserRepositoryInterface { + /** * Returns a collection of all users. * @@ -41,6 +42,14 @@ interface UserRepositoryInterface */ public function attachRole(User $user, string $role): bool; + /** + * @param User $user + * @param string $password + * + * @return mixed + */ + public function changePassword(User $user, string $password); + /** * Returns a count of all users. * @@ -48,6 +57,13 @@ interface UserRepositoryInterface */ public function count(): int; + /** + * @param User $user + * + * @return bool + */ + public function destroy(User $user): bool; + /** * @param int $userId * diff --git a/app/Rules/Actions/ClearCategory.php b/app/Rules/Actions/ClearCategory.php index 5af30a1811..d60afcf77a 100644 --- a/app/Rules/Actions/ClearCategory.php +++ b/app/Rules/Actions/ClearCategory.php @@ -16,6 +16,7 @@ namespace FireflyIII\Rules\Actions; use FireflyIII\Models\RuleAction; use FireflyIII\Models\TransactionJournal; +use Log; /** * Class ClearCategory diff --git a/app/Rules/Actions/SetBudget.php b/app/Rules/Actions/SetBudget.php index 90d7aa6d18..e9f8c1fae9 100644 --- a/app/Rules/Actions/SetBudget.php +++ b/app/Rules/Actions/SetBudget.php @@ -50,7 +50,8 @@ class SetBudget implements ActionInterface public function act(TransactionJournal $journal): bool { /** @var BudgetRepositoryInterface $repository */ - $repository = app(BudgetRepositoryInterface::class, [$journal->user]); + $repository = app(BudgetRepositoryInterface::class); + $repository->setUser($journal->user); $search = $this->action->action_value; $budgets = $repository->getActiveBudgets(); $budget = $budgets->filter( diff --git a/app/Rules/Actions/SetDestinationAccount.php b/app/Rules/Actions/SetDestinationAccount.php new file mode 100644 index 0000000000..25677e546b --- /dev/null +++ b/app/Rules/Actions/SetDestinationAccount.php @@ -0,0 +1,142 @@ +action = $action; + } + + /** + * @param TransactionJournal $journal + * + * @return bool + */ + public function act(TransactionJournal $journal): bool + { + $this->journal = $journal; + $this->repository = app(AccountRepositoryInterface::class); + $this->repository->setUser($journal->user); + $count = $journal->transactions()->count(); + if ($count > 2) { + Log::error(sprintf('Cannot change destination account of journal #%d because it is a split journal.', $journal->id)); + + return true; + } + + // journal type: + $type = $journal->transactionType->type; + + // if this is a deposit or a transfer, the destination account must be an asset account or a default account, and it MUST exist: + if (($type === TransactionType::DEPOSIT || $type === TransactionType::TRANSFER) && !$this->findAssetAccount()) { + Log::error( + sprintf( + 'Cannot change destination account of journal #%d because no asset account with name "%s" exists.', + $journal->id, $this->action->action_value + ) + ); + + return true; + } + + // if this is a withdrawal, the new destination account must be a expense account and may be created: + if ($type === TransactionType::WITHDRAWAL) { + $this->findExpenseAccount(); + } + + Log::debug(sprintf('New destination account is #%d ("%s").', $this->newDestinationAccount->id, $this->newDestinationAccount->name)); + + // update destination transaction with new destination account: + // get destination transaction: + $transaction = $journal->transactions()->where('amount', '>', 0)->first(); + $transaction->account_id = $this->newDestinationAccount->id; + $transaction->save(); + Log::debug(sprintf('Updated transaction #%d and gave it new account ID.', $transaction->id)); + + return true; + } + + /** + * @return bool + */ + private function findAssetAccount(): bool + { + $account = $this->repository->findByName($this->action->action_value, [AccountType::DEFAULT, AccountType::ASSET]); + + if (is_null($account->id)) { + Log::debug(sprintf('There is NO asset account called "%s".', $this->action->action_value)); + + return false; + } + Log::debug(sprintf('There exists an asset account called "%s". ID is #%d', $this->action->action_value, $account->id)); + $this->newDestinationAccount = $account; + + return true; + } + + /** + * + */ + private function findExpenseAccount() + { + $account = $this->repository->findByName($this->action->action_value, [AccountType::EXPENSE]); + if (is_null($account->id)) { + // create new revenue account with this name: + $data = [ + 'name' => $this->action->action_value, + 'accountType' => 'expense', + 'virtualBalance' => 0, + 'active' => true, + 'iban' => null, + ]; + $account = $this->repository->store($data); + } + Log::debug(sprintf('Found or created expense account #%d ("%s")', $account->id, $account->name)); + $this->newDestinationAccount = $account; + } +} diff --git a/app/Rules/Actions/SetSourceAccount.php b/app/Rules/Actions/SetSourceAccount.php new file mode 100644 index 0000000000..fc8067856c --- /dev/null +++ b/app/Rules/Actions/SetSourceAccount.php @@ -0,0 +1,141 @@ +action = $action; + } + + /** + * @param TransactionJournal $journal + * + * @return bool + */ + public function act(TransactionJournal $journal): bool + { + $this->journal = $journal; + $this->repository = app(AccountRepositoryInterface::class); + $this->repository->setUser($journal->user); + $count = $journal->transactions()->count(); + if ($count > 2) { + Log::error(sprintf('Cannot change source account of journal #%d because it is a split journal.', $journal->id)); + + return true; + } + + // journal type: + $type = $journal->transactionType->type; + // if this is a transfer or a withdrawal, the new source account must be an asset account or a default account, and it MUST exist: + if (($type === TransactionType::WITHDRAWAL || $type === TransactionType::TRANSFER) && !$this->findAssetAccount()) { + Log::error( + sprintf( + 'Cannot change source account of journal #%d because no asset account with name "%s" exists.', + $journal->id, $this->action->action_value + ) + ); + + return true; + } + + // if this is a deposit, the new source account must be a revenue account and may be created: + if ($type === TransactionType::DEPOSIT) { + $this->findRevenueAccount(); + } + + Log::debug(sprintf('New source account is #%d ("%s").', $this->newSourceAccount->id, $this->newSourceAccount->name)); + + // update source transaction with new source account: + // get source transaction: + $transaction = $journal->transactions()->where('amount', '<', 0)->first(); + $transaction->account_id = $this->newSourceAccount->id; + $transaction->save(); + Log::debug(sprintf('Updated transaction #%d and gave it new account ID.', $transaction->id)); + + return true; + } + + /** + * @return bool + */ + private function findAssetAccount(): bool + { + $account = $this->repository->findByName($this->action->action_value, [AccountType::DEFAULT, AccountType::ASSET]); + + if (is_null($account->id)) { + Log::debug(sprintf('There is NO asset account called "%s".', $this->action->action_value)); + + return false; + } + Log::debug(sprintf('There exists an asset account called "%s". ID is #%d', $this->action->action_value, $account->id)); + $this->newSourceAccount = $account; + + return true; + } + + /** + * + */ + private function findRevenueAccount() + { + $account = $this->repository->findByName($this->action->action_value, [AccountType::REVENUE]); + if (is_null($account->id)) { + // create new revenue account with this name: + $data = [ + 'name' => $this->action->action_value, + 'accountType' => 'revenue', + 'virtualBalance' => 0, + 'active' => true, + 'iban' => null, + ]; + $account = $this->repository->store($data); + } + Log::debug(sprintf('Found or created revenue account #%d ("%s")', $account->id, $account->name)); + $this->newSourceAccount = $account; + } +} diff --git a/app/Rules/Factory/ActionFactory.php b/app/Rules/Factory/ActionFactory.php index a73c1c23b2..9bbdb7468d 100644 --- a/app/Rules/Factory/ActionFactory.php +++ b/app/Rules/Factory/ActionFactory.php @@ -10,13 +10,6 @@ */ declare(strict_types = 1); -/** - * ActionFactory.php - * Copyright (C) 2016 Robert Horlings - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ namespace FireflyIII\Rules\Factory; diff --git a/app/Rules/Processor.php b/app/Rules/Processor.php index d7192cddf9..9b0daf0ae9 100644 --- a/app/Rules/Processor.php +++ b/app/Rules/Processor.php @@ -16,6 +16,7 @@ namespace FireflyIII\Rules; use FireflyIII\Models\Rule; use FireflyIII\Models\RuleAction; use FireflyIII\Models\RuleTrigger; +use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Rules\Actions\ActionInterface; use FireflyIII\Rules\Factory\ActionFactory; @@ -144,6 +145,36 @@ final class Processor return $this->rule; } + /** + * This method will scan the given transaction journal and check if it matches the triggers found in the Processor + * If so, it will also attempt to run the given actions on the journal. It returns a bool indicating if the transaction journal + * matches all of the triggers (regardless of whether the Processor could act on it). + * + * @param Transaction $transaction + * + * @return bool + */ + public function handleTransaction(Transaction $transaction): bool + { + Log::debug(sprintf('handleTransactionJournal for journal #%d (transaction #%d)', $transaction->transaction_journal_id, $transaction->id)); + + // grab the actual journal. + $journal = $transaction->transactionJournal()->first(); + $this->journal = $journal; + // get all triggers: + $triggered = $this->triggered(); + if ($triggered) { + if ($this->actions->count() > 0) { + $this->actions(); + } + + return true; + } + + return false; + + } + /** * This method will scan the given transaction journal and check if it matches the triggers found in the Processor * If so, it will also attempt to run the given actions on the journal. It returns a bool indicating if the transaction journal diff --git a/app/Rules/TransactionMatcher.php b/app/Rules/TransactionMatcher.php index 7491bbbb88..5241e4f84c 100644 --- a/app/Rules/TransactionMatcher.php +++ b/app/Rules/TransactionMatcher.php @@ -13,7 +13,8 @@ declare(strict_types = 1); namespace FireflyIII\Rules; -use FireflyIII\Models\TransactionJournal; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Journal\JournalTaskerInterface; use Illuminate\Support\Collection; @@ -62,7 +63,7 @@ class TransactionMatcher if (count($this->triggers) === 0) { return new Collection; } - $pagesize = min($this->range / 2, $this->limit * 2); + $pageSize = min($this->range / 2, $this->limit * 2); // Variables used within the loop $processed = 0; @@ -76,31 +77,44 @@ class TransactionMatcher // - the maximum number of transactions to search in have been searched do { // Fetch a batch of transactions from the database - $paginator = $this->tasker->getJournals($this->transactionTypes, $page, $pagesize); - $set = $paginator->getCollection(); - + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser(auth()->user()); + $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->setTypes($this->transactionTypes); + $set = $collector->getPaginatedJournals(); + Log::debug(sprintf('Found %d journals to check. ', $set->count())); // Filter transactions that match the given triggers. $filtered = $set->filter( - function (TransactionJournal $journal) use ($processor) { - Log::debug(sprintf('Test these triggers on #%d', $journal->id)); + function (Transaction $transaction) use ($processor) { + Log::debug(sprintf('Test these triggers on journal #%d (transaction #%d)', $transaction->transaction_journal_id, $transaction->id)); - return $processor->handleTransactionJournal($journal); + return $processor->handleTransaction($transaction); } ); + Log::debug(sprintf('Found %d journals that match.', $filtered->count())); + // merge: /** @var Collection $result */ $result = $result->merge($filtered); + Log::debug(sprintf('Total count is now %d', $result->count())); // Update counters $page++; $processed += count($set); + Log::debug(sprintf('Page is now %d, processed is %d', $page, $processed)); + // Check for conditions to finish the loop - $reachedEndOfList = $set->count() < $pagesize; + $reachedEndOfList = $set->count() < 1; $foundEnough = $result->count() >= $this->limit; $searchedEnough = ($processed >= $this->range); + + Log::debug(sprintf('reachedEndOfList: %s', var_export($reachedEndOfList, true))); + Log::debug(sprintf('foundEnough: %s', var_export($foundEnough, true))); + Log::debug(sprintf('searchedEnough: %s', var_export($searchedEnough, true))); + } while (!$reachedEndOfList && !$foundEnough && !$searchedEnough); // If the list of matchingTransactions is larger than the maximum number of results diff --git a/app/Rules/Triggers/AbstractTrigger.php b/app/Rules/Triggers/AbstractTrigger.php index f3be138f8a..0fd9a3bfd8 100644 --- a/app/Rules/Triggers/AbstractTrigger.php +++ b/app/Rules/Triggers/AbstractTrigger.php @@ -74,6 +74,8 @@ class AbstractTrigger return $self; } + + /** * @param RuleTrigger $trigger * @param TransactionJournal $journal diff --git a/app/Rules/Triggers/AmountExactly.php b/app/Rules/Triggers/AmountExactly.php index cef4d8315d..e7984deb02 100644 --- a/app/Rules/Triggers/AmountExactly.php +++ b/app/Rules/Triggers/AmountExactly.php @@ -60,7 +60,7 @@ final class AmountExactly extends AbstractTrigger implements TriggerInterface { $amount = $journal->destination_amount ?? TransactionJournal::amountPositive($journal); $compare = $this->triggerValue; - $result = bccomp($amount, $compare, 4); + $result = bccomp($amount, $compare); if ($result === 0) { Log::debug(sprintf('RuleTrigger AmountExactly for journal #%d: %d matches %d exactly, so return true', $journal->id, $amount, $compare)); diff --git a/app/Rules/Triggers/AmountLess.php b/app/Rules/Triggers/AmountLess.php index cbf4e61ac3..1c856c6796 100644 --- a/app/Rules/Triggers/AmountLess.php +++ b/app/Rules/Triggers/AmountLess.php @@ -60,7 +60,7 @@ final class AmountLess extends AbstractTrigger implements TriggerInterface { $amount = $journal->destination_amount ?? TransactionJournal::amountPositive($journal); $compare = $this->triggerValue; - $result = bccomp($amount, $compare, 4); + $result = bccomp($amount, $compare); if ($result === -1) { Log::debug(sprintf('RuleTrigger AmountLess for journal #%d: %d is less than %d, so return true', $journal->id, $amount, $compare)); diff --git a/app/Rules/Triggers/AmountMore.php b/app/Rules/Triggers/AmountMore.php index d44cca0071..d8a7ce1643 100644 --- a/app/Rules/Triggers/AmountMore.php +++ b/app/Rules/Triggers/AmountMore.php @@ -66,7 +66,7 @@ final class AmountMore extends AbstractTrigger implements TriggerInterface { $amount = $journal->destination_amount ?? TransactionJournal::amountPositive($journal); $compare = $this->triggerValue; - $result = bccomp($amount, $compare, 4); + $result = bccomp($amount, $compare); if ($result === 1) { Log::debug(sprintf('RuleTrigger AmountMore for journal #%d: %d is more than %d, so return true', $journal->id, $amount, $compare)); diff --git a/app/Rules/Triggers/BudgetIs.php b/app/Rules/Triggers/BudgetIs.php new file mode 100644 index 0000000000..fa444f3ffc --- /dev/null +++ b/app/Rules/Triggers/BudgetIs.php @@ -0,0 +1,95 @@ +budgets()->first(); + if (!is_null($budget)) { + $name = strtolower($budget->name); + // match on journal: + if ($name === strtolower($this->triggerValue)) { + Log::debug(sprintf('RuleTrigger BudgetIs for journal #%d: "%s" is "%s", return true.', $journal->id, $name, $this->triggerValue)); + + return true; + } + } + + if (is_null($budget)) { + // perhaps transactions have this budget? + /** @var Transaction $transaction */ + foreach ($journal->transactions as $transaction) { + $budget = $transaction->budgets()->first(); + if (!is_null($budget)) { + $name = strtolower($budget->name); + if ($name === strtolower($this->triggerValue)) { + Log::debug( + sprintf( + 'RuleTrigger BudgetIs for journal #%d (transaction #%d): "%s" is "%s", return true.', + $journal->id, $transaction->id, $name, $this->triggerValue + ) + ); + + return true; + } + } + } + } + + Log::debug(sprintf('RuleTrigger BudgetIs for journal #%d: does not have budget "%s", return false.', $journal->id, $this->triggerValue)); + + return false; + } +} diff --git a/app/Rules/Triggers/CategoryIs.php b/app/Rules/Triggers/CategoryIs.php new file mode 100644 index 0000000000..28dcddb086 --- /dev/null +++ b/app/Rules/Triggers/CategoryIs.php @@ -0,0 +1,95 @@ +categories()->first(); + if (!is_null($category)) { + $name = strtolower($category->name); + // match on journal: + if ($name === strtolower($this->triggerValue)) { + Log::debug(sprintf('RuleTrigger CategoryIs for journal #%d: "%s" is "%s", return true.', $journal->id, $name, $this->triggerValue)); + + return true; + } + } + + if (is_null($category)) { + // perhaps transactions have this category? + /** @var Transaction $transaction */ + foreach ($journal->transactions as $transaction) { + $category = $transaction->categories()->first(); + if (!is_null($category)) { + $name = strtolower($category->name); + if ($name === strtolower($this->triggerValue)) { + Log::debug( + sprintf( + 'RuleTrigger CategoryIs for journal #%d (transaction #%d): "%s" is "%s", return true.', + $journal->id, $transaction->id, $name, $this->triggerValue + ) + ); + + return true; + } + } + } + } + + Log::debug(sprintf('RuleTrigger CategoryIs for journal #%d: does not have category "%s", return false.', $journal->id, $this->triggerValue)); + + return false; + } +} diff --git a/app/Rules/Triggers/TagIs.php b/app/Rules/Triggers/TagIs.php new file mode 100644 index 0000000000..684d806760 --- /dev/null +++ b/app/Rules/Triggers/TagIs.php @@ -0,0 +1,74 @@ +tags()->get(); + /** @var Tag $tag */ + foreach ($tags as $tag) { + $name = strtolower($tag->tag); + // match on journal: + if ($name === strtolower($this->triggerValue)) { + Log::debug(sprintf('RuleTrigger TagIs for journal #%d: is tagged with "%s", return true.', $journal->id, $name)); + + return true; + } + } + Log::debug(sprintf('RuleTrigger TagIs for journal #%d: is not tagged with "%s", return false.', $journal->id, $this->triggerValue)); + + return false; + } +} diff --git a/app/Support/Amount.php b/app/Support/Amount.php index 7937ae148a..9434352b80 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -17,7 +17,7 @@ use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use Illuminate\Support\Collection; -use NumberFormatter; +use Log; use Preferences as Prefs; /** @@ -27,6 +27,80 @@ use Preferences as Prefs; */ class Amount { + /** + * bool $sepBySpace is $localeconv['n_sep_by_space'] + * int $signPosn = $localeconv['n_sign_posn'] + * string $sign = $localeconv['negative_sign'] + * bool $csPrecedes = $localeconv['n_cs_precedes'] + * + * @param bool $sepBySpace + * @param int $signPosn + * @param string $sign + * @param bool $csPrecedes + * + * @return string + */ + public static function getAmountJsConfig(bool $sepBySpace, int $signPosn, string $sign, bool $csPrecedes): string + { + // negative first: + $space = ' '; + + // require space between symbol and amount? + if (!$sepBySpace) { + $space = ''; // no + } + + // there are five possible positions for the "+" or "-" sign (if it is even used) + // pos_a and pos_e could be the ( and ) symbol. + $pos_a = ''; // before everything + $pos_b = ''; // before currency symbol + $pos_c = ''; // after currency symbol + $pos_d = ''; // before amount + $pos_e = ''; // after everything + + // format would be (currency before amount) + // AB%sC_D%vE + // or: + // AD%v_B%sCE (amount before currency) + // the _ is the optional space + + + // switch on how to display amount: + switch ($signPosn) { + default: + case 0: + // ( and ) around the whole thing + $pos_a = '('; + $pos_e = ')'; + break; + case 1: + // The sign string precedes the quantity and currency_symbol + $pos_a = $sign; + break; + case 2: + // The sign string succeeds the quantity and currency_symbol + $pos_e = $sign; + break; + case 3: + // The sign string immediately precedes the currency_symbol + $pos_b = $sign; + break; + case 4: + // The sign string immediately succeeds the currency_symbol + $pos_c = $sign; + } + + // default is amount before currency + $format = $pos_a . $pos_d . '%v' . $space . $pos_b . '%s' . $pos_c . $pos_e; + + if ($csPrecedes) { + // alternative is currency before amount + $format = $pos_a . $pos_b . '%s' . $pos_c . $space . $pos_d . '%v' . $pos_e; + } + Log::debug(sprintf('Final format: "%s"', $format)); + + return $format; + } /** * @param string $amount @@ -43,30 +117,42 @@ class Amount * 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. * - * @param TransactionCurrency $format - * @param string $amount - * @param bool $coloured + * @param \FireflyIII\Models\TransactionCurrency $format + * @param string $amount + * @param bool $coloured * * @return string */ public function formatAnything(TransactionCurrency $format, string $amount, bool $coloured = true): string { - $locale = setlocale(LC_MONETARY, 0); - $float = round($amount, 2); - $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY); - $result = $formatter->formatCurrency($float, $format->code); + $locale = explode(',', trans('config.locale')); + $locale = array_map('trim', $locale); + setlocale(LC_MONETARY, $locale); + $float = round($amount, 12); + $info = localeconv(); + $formatted = number_format($float, $format->decimal_places, $info['mon_decimal_point'], $info['mon_thousands_sep']); + + // some complicated switches to format the amount correctly: + $precedes = $amount < 0 ? $info['n_cs_precedes'] : $info['p_cs_precedes']; + $separated = $amount < 0 ? $info['n_sep_by_space'] : $info['p_sep_by_space']; + $space = $separated ? ' ' : ''; + $result = $format->symbol . $space . $formatted; + + if (!$precedes) { + $result = $space . $formatted . $format->symbol; + } if ($coloured === true) { if ($amount > 0) { - return '' . $result . ''; + return sprintf('%s', $result); } else { if ($amount < 0) { - return '' . $result . ''; + return sprintf('%s', $result); } } - return '' . $result . ''; + return sprintf('%s', $result); } @@ -74,6 +160,22 @@ 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::whereCode($currencyCode)->first(); + + return $this->formatAnything($currency, $amount, $coloured); + } + /** * * @param \FireflyIII\Models\TransactionJournal $journal @@ -83,27 +185,9 @@ class Amount */ public function formatJournal(TransactionJournal $journal, bool $coloured = true): string { - $locale = setlocale(LC_MONETARY, 0); - $float = round(TransactionJournal::amount($journal), 2); - $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY); - $currencyCode = $journal->transaction_currency_code ?? $journal->transactionCurrency->code; - $result = $formatter->formatCurrency($float, $currencyCode); + $currency = $journal->transactionCurrency; - if ($coloured === true && $float === 0.00) { - return '' . $result . ''; // always grey. - } - if (!$coloured) { - return $result; - } - if (!$journal->isTransfer()) { - if ($float > 0) { - return '' . $result . ''; - } - - return '' . $result . ''; - } else { - return '' . $result . ''; - } + return $this->formatAnything($currency, TransactionJournal::amount($journal), $coloured); } /** @@ -119,41 +203,6 @@ class Amount return $this->formatAnything($currency, strval($transaction->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 currency code. - * - * @param string $code - * @param string $amount - * @param bool $coloured - * - * @return string - */ - public function formatWithCode(string $code, string $amount, bool $coloured = true): string - { - $locale = setlocale(LC_MONETARY, 0); - $float = round($amount, 2); - $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY); - $result = $formatter->formatCurrency($float, $code); - - if ($coloured === true) { - - if ($amount > 0) { - return '' . $result . ''; - } else { - if ($amount < 0) { - return '' . $result . ''; - } - } - - return '' . $result . ''; - - - } - - return $result; - } - /** * @return Collection */ @@ -223,4 +272,24 @@ class Amount return $currency; } + + /** + * This method returns the correct format rules required by accounting.js, + * the library used to format amounts in charts. + * + * @param array $config + * + * @return array + */ + public function getJsConfig(array $config): array + { + $negative = self::getAmountJsConfig($config['n_sep_by_space'] === 1, $config['n_sign_posn'], $config['negative_sign'], $config['n_cs_precedes'] === 1); + $positive = self::getAmountJsConfig($config['p_sep_by_space'] === 1, $config['p_sign_posn'], $config['positive_sign'], $config['p_cs_precedes'] === 1); + + return [ + 'pos' => $positive, + 'neg' => $negative, + 'zero' => $positive, + ]; + } } diff --git a/app/Support/Binder/AccountList.php b/app/Support/Binder/AccountList.php index 1512f0bbfa..da684a089f 100644 --- a/app/Support/Binder/AccountList.php +++ b/app/Support/Binder/AccountList.php @@ -47,6 +47,12 @@ class AccountList implements BinderInterface ->where('user_id', auth()->user()->id) ->get(['accounts.*']); if ($object->count() > 0) { + $object = $object->sortBy( + function (Account $account) { + return $account->name; + } + ); + return $object; } } diff --git a/app/Support/Binder/TagList.php b/app/Support/Binder/TagList.php new file mode 100644 index 0000000000..b1c42bb4c8 --- /dev/null +++ b/app/Support/Binder/TagList.php @@ -0,0 +1,52 @@ +check()) { + $tags = explode(',', $value); + /** @var TagRepositoryInterface $repository */ + $repository = app(TagRepositoryInterface::class); + $allTags = $repository->get(); + $set = $allTags->filter( + function (Tag $tag) use ($tags) { + return in_array($tag->tag, $tags); + } + ); + + if ($set->count() > 0) { + return $set; + } + } + throw new NotFoundHttpException; + } +} diff --git a/app/Support/CacheProperties.php b/app/Support/CacheProperties.php index 7ed1c1fdfb..b8e493d134 100644 --- a/app/Support/CacheProperties.php +++ b/app/Support/CacheProperties.php @@ -18,6 +18,7 @@ use Cache; use Carbon\Carbon; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Support\Collection; +use Log; use Preferences as Prefs; /** @@ -95,6 +96,7 @@ class CacheProperties */ private function md5() { + $this->md5 = ''; foreach ($this->properties as $property) { if ($property instanceof Collection || $property instanceof EloquentCollection) { @@ -108,10 +110,17 @@ class CacheProperties if (is_object($property)) { $this->md5 .= $property->__toString(); } + if (is_bool($property) && $property === false) { + $this->md5 .= 'false'; + } + if (is_bool($property) && $property === true) { + $this->md5 .= 'true'; + } $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/ChartColour.php b/app/Support/ChartColour.php new file mode 100644 index 0000000000..c04b75b28e --- /dev/null +++ b/app/Support/ChartColour.php @@ -0,0 +1,56 @@ +label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = $this->fillFieldValue($name, $value); - $options['step'] = 'any'; - $options['min'] = '0.01'; - $defaultCurrency = isset($options['currency']) ? $options['currency'] : Amt::getDefaultCurrency(); - $currencies = Amt::getAllCurrencies(); - unset($options['currency']); - unset($options['placeholder']); - $html = view('form.amount', compact('defaultCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); - - return $html; - + return $this->currencyField($name, 'amount', $value, $options); } /** @@ -65,20 +52,7 @@ class ExpandedForm */ public function amountSmall(string $name, $value = null, array $options = []): string { - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = $this->fillFieldValue($name, $value); - $options['step'] = 'any'; - $options['min'] = '0.01'; - $defaultCurrency = isset($options['currency']) ? $options['currency'] : Amt::getDefaultCurrency(); - $currencies = Amt::getAllCurrencies(); - unset($options['currency']); - unset($options['placeholder']); - $html = view('form.amount-small', compact('defaultCurrency', 'currencies', 'classes', 'name', 'value', 'options'))->render(); - - return $html; - + return $this->currencyField($name, 'amount-small', $value, $options); } /** @@ -90,18 +64,7 @@ class ExpandedForm */ public function balance(string $name, $value = null, array $options = []): string { - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = round($this->fillFieldValue($name, $value), 2); - $options['step'] = 'any'; - $defaultCurrency = isset($options['currency']) ? $options['currency'] : Amt::getDefaultCurrency(); - $currencies = Amt::getAllCurrencies(); - unset($options['currency']); - unset($options['placeholder']); - $html = view('form.balance', compact('defaultCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); - - return $html; + return $this->currencyField($name, 'balance', $value, $options); } /** @@ -320,6 +283,22 @@ class ExpandedForm return $html; } + /** + * @param $name + * @param array $options + * + * @return string + */ + public function password(string $name, array $options = []): string + { + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $html = view('form.password', compact('classes', 'name', 'label', 'value', 'options'))->render(); + + return $html; + + } /** * @param $name @@ -500,4 +479,35 @@ class ExpandedForm return strval(trans('form.' . $name)); } + + /** + * @param string $name + * @param string $view + * @param null $value + * @param array $options + * + * @return string + */ + private function currencyField(string $name, string $view, $value = null, array $options = []): string + { + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); + $options['step'] = 'any'; + $defaultCurrency = isset($options['currency']) ? $options['currency'] : Amt::getDefaultCurrency(); + $currencies = Amt::getAllCurrencies(); + unset($options['currency']); + unset($options['placeholder']); + + // make sure value is formatted nicely: + if (!is_null($value) && $value !== '') { + $value = round($value, $defaultCurrency->decimal_places); + } + + + $html = view('form.' . $view, compact('defaultCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); + + return $html; + } } diff --git a/app/Support/FireflyConfig.php b/app/Support/FireflyConfig.php index 32ff7958c3..743dc40c2a 100644 --- a/app/Support/FireflyConfig.php +++ b/app/Support/FireflyConfig.php @@ -45,15 +45,12 @@ class FireflyConfig * @param $name * @param null $default * - * @return Configuration|null + * @return \FireflyIII\Models\Configuration|null */ public function get($name, $default = null) { - Log::debug('Now in FFConfig::get()', ['name' => $name]); $fullName = 'ff-config-' . $name; if (Cache::has($fullName)) { - Log::debug('Return cache.'); - return Cache::get($fullName); } @@ -61,22 +58,15 @@ class FireflyConfig if ($config) { Cache::forever($fullName, $config); - Log::debug('Return found one.'); return $config; } // no preference found and default is null: if (is_null($default)) { - // return NULL - Log::debug('Return null.'); - return null; } - Log::debug('Return this->set().'); - return $this->set($name, $default); - } /** @@ -91,12 +81,12 @@ class FireflyConfig } /** - * @param $name - * @param string $value + * @param string $name + * @param $value * * @return Configuration */ - public function set($name, $value): Configuration + public function set(string $name, $value): Configuration { Log::debug('Set new value for ', ['name' => $name]); $config = Configuration::whereName($name)->first(); diff --git a/app/Support/Migration/TestData.php b/app/Support/Migration/TestData.php deleted file mode 100644 index 97c46c98d0..0000000000 --- a/app/Support/Migration/TestData.php +++ /dev/null @@ -1,868 +0,0 @@ -data = $data; - $start = new Carbon; - $start->startOfYear(); - $start->subYears(2); - $end = new Carbon; - - $this->start = $start; - $this->end = $end; - $this->time = $end->format('Y-m-d H:i:s'); - } - - /** - * @param array $data - */ - public static function run(array $data) - { - $seeder = new TestData($data); - $seeder->go(); - } - - /** - * - */ - private function createAccounts() - { - $insert = []; - foreach ($this->data['accounts'] as $account) { - $insert[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $account['user_id'], - 'account_type_id' => $account['account_type_id'], - 'name' => $account['name'], - 'active' => 1, - 'encrypted' => 0, - 'virtual_balance' => 0, - 'iban' => isset($account['iban']) ? Crypt::encrypt($account['iban']) : null, - ]; - } - DB::table('accounts')->insert($insert); - $insert = []; - foreach ($this->data['account-meta'] as $meta) { - $insert[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'account_id' => $meta['account_id'], - 'name' => $meta['name'], - 'data' => $meta['data'], - ]; - } - DB::table('account_meta')->insert($insert); - } - - /** - * - */ - private function createAttachments() - { - $disk = Storage::disk('upload'); - foreach ($this->data['attachments'] as $attachment) { - $data = Crypt::encrypt($attachment['content']); - $attachmentId = DB::table('attachments')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'attachable_id' => $attachment['attachable_id'], - 'attachable_type' => $attachment['attachable_type'], - 'user_id' => $attachment['user_id'], - 'md5' => md5($attachment['content']), - 'filename' => Crypt::encrypt($attachment['filename']), - 'title' => Crypt::encrypt($attachment['title']), - 'description' => Crypt::encrypt($attachment['description']), - 'notes' => Crypt::encrypt($attachment['notes']), - 'mime' => $attachment['mime'], - 'size' => strlen($attachment['content']), - 'uploaded' => 1, - ] - ); - - $disk->put('at-' . $attachmentId . '.data', $data); - } - } - - /** - * - */ - private function createBills() - { - $insert = []; - foreach ($this->data['bills'] as $bill) { - $insert[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $bill['user_id'], - 'name' => Crypt::encrypt($bill['name']), - 'match' => Crypt::encrypt($bill['match']), - 'amount_min' => $bill['amount_min'], - 'amount_max' => $bill['amount_max'], - 'date' => $bill['date'], - 'active' => $bill['active'], - 'automatch' => $bill['automatch'], - 'repeat_freq' => $bill['repeat_freq'], - 'skip' => $bill['skip'], - 'name_encrypted' => 1, - 'match_encrypted' => 1, - ]; - } - DB::table('bills')->insert($insert); - } - - /** - * - */ - private function createBudgets() - { - $insert = []; - foreach ($this->data['budgets'] as $budget) { - $insert[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $budget['user_id'], - 'name' => Crypt::encrypt($budget['name']), - 'encrypted' => 1, - ]; - } - DB::table('budgets')->insert($insert); - - foreach ($this->data['budget-limits'] as $limit) { - $amount = rand($limit['amount_min'], $limit['amount_max']); - $limitId = DB::table('budget_limits')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'budget_id' => $limit['budget_id'], - 'startdate' => $limit['startdate'], - 'amount' => $amount, - 'repeats' => 0, - 'repeat_freq' => $limit['repeat_freq'], - ] - ); - - DB::table('limit_repetitions')->insert( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'budget_limit_id' => $limitId, - 'startdate' => $limit['startdate'], - 'enddate' => Navigation::endOfPeriod(new Carbon($limit['startdate']), $limit['repeat_freq'])->format('Y-m-d'), - 'amount' => $amount, - ] - ); - } - $current = clone $this->start; - while ($current <= $this->end) { - foreach ($this->data['monthly-limits'] as $limit) { - $amount = rand($limit['amount_min'], $limit['amount_max']); - $limitId = DB::table('budget_limits')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'budget_id' => $limit['budget_id'], - 'startdate' => $current->format('Y-m-d'), - 'amount' => $amount, - 'repeats' => 0, - 'repeat_freq' => 'monthly', - ] - ); - - DB::table('limit_repetitions')->insert( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'budget_limit_id' => $limitId, - 'startdate' => $current->format('Y-m-d'), - 'enddate' => Navigation::endOfPeriod($current, 'monthly')->format('Y-m-d'), - 'amount' => $amount, - ] - ); - } - - $current->addMonth(); - } - - } - - /** - * - */ - private function createCategories() - { - $insert = []; - foreach ($this->data['categories'] as $category) { - $insert[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $category['user_id'], - 'name' => Crypt::encrypt($category['name']), - 'encrypted' => 1, - ]; - } - DB::table('categories')->insert($insert); - } - - /** - * - */ - private function createCurrencies() - { - $insert = []; - foreach ($this->data['currencies'] as $job) { - $insert[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'deleted_at' => null, - 'code' => $job['code'], - 'name' => $job['name'], - 'symbol' => $job['symbol'], - ]; - } - DB::table('transaction_currencies')->insert($insert); - } - - /** - * - */ - private function createImportJobs() - { - $insert = []; - foreach ($this->data['import-jobs'] as $job) { - $insert[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $job['user_id'], - 'file_type' => $job['file_type'], - 'key' => $job['key'], - 'status' => $job['status'], - 'extended_status' => json_encode($job['extended_status']), - 'configuration' => json_encode($job['configuration']), - ]; - } - DB::table('import_jobs')->insert($insert); - } - - /** - * - */ - private function createJournals() - { - $current = clone $this->start; - $transactions = []; - while ($current <= $this->end) { - $date = $current->format('Y-m-'); - $month = $current->format('F'); - - // run all monthly withdrawals: - foreach ($this->data['monthly-withdrawals'] as $withdrawal) { - $description = str_replace(':month', $month, $withdrawal['description']); - $journalId = DB::table('transaction_journals')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $withdrawal['user_id'], - 'transaction_type_id' => 1, - 'bill_id' => $withdrawal['bill_id'] ?? null, - 'transaction_currency_id' => 1, - 'description' => Crypt::encrypt($description), - 'completed' => 1, - 'date' => $date . $withdrawal['day-of-month'], - 'interest_date' => $withdrawal['interest_date'] ?? null, - 'book_date' => $withdrawal['book_date'] ?? null, - 'process_date' => $withdrawal['process_date'] ?? null, - 'encrypted' => 1, - 'order' => 0, - 'tag_count' => 0, - ] - ); - $amount = (rand($withdrawal['min_amount'] * 100, $withdrawal['max_amount'] * 100)) / 100; - $transactions[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'transaction_journal_id' => $journalId, - 'account_id' => $withdrawal['source_id'], - 'amount' => $amount * -1, - ]; - $transactions[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'transaction_journal_id' => $journalId, - 'account_id' => $withdrawal['destination_id'], - 'amount' => $amount, - ]; - - // link to budget if set. - if (isset($withdrawal['budget_id'])) { - DB::table('budget_transaction_journal')->insert( - [ - 'budget_id' => $withdrawal['budget_id'], - 'transaction_journal_id' => $journalId, - - ] - ); - } - // link to category if set. - if (isset($withdrawal['category_id'])) { - DB::table('category_transaction_journal')->insert( - [ - 'category_id' => $withdrawal['category_id'], - 'transaction_journal_id' => $journalId, - - ] - ); - } - } - - // run all monthly deposits: - foreach ($this->data['monthly-deposits'] as $deposit) { - $description = str_replace(':month', $month, $deposit['description']); - $journalId = DB::table('transaction_journals')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $deposit['user_id'], - 'transaction_type_id' => 2, - 'bill_id' => $deposit['bill_id'] ?? null, - 'transaction_currency_id' => 1, - 'description' => Crypt::encrypt($description), - 'completed' => 1, - 'date' => $date . $deposit['day-of-month'], - 'interest_date' => $deposit['interest_date'] ?? null, - 'book_date' => $deposit['book_date'] ?? null, - 'process_date' => $deposit['process_date'] ?? null, - 'encrypted' => 1, - 'order' => 0, - 'tag_count' => 0, - ] - ); - $amount = (rand($deposit['min_amount'] * 100, $deposit['max_amount'] * 100)) / 100; - $transactions[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'transaction_journal_id' => $journalId, - 'account_id' => $deposit['source_id'], - 'amount' => $amount * -1, - ]; - $transactions[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'transaction_journal_id' => $journalId, - 'account_id' => $deposit['destination_id'], - 'amount' => $amount, - ]; - - // link to category if set. - if (isset($deposit['category_id'])) { - DB::table('category_transaction_journal')->insert( - [ - 'category_id' => $deposit['category_id'], - 'transaction_journal_id' => $journalId, - - ] - ); - } - } - // run all monthly transfers: - foreach ($this->data['monthly-transfers'] as $transfer) { - $description = str_replace(':month', $month, $transfer['description']); - $journalId = DB::table('transaction_journals')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $transfer['user_id'], - 'transaction_type_id' => 3, - 'bill_id' => $transfer['bill_id'] ?? null, - 'transaction_currency_id' => 1, - 'description' => Crypt::encrypt($description), - 'completed' => 1, - 'date' => $date . $transfer['day-of-month'], - 'interest_date' => $transfer['interest_date'] ?? null, - 'book_date' => $transfer['book_date'] ?? null, - 'process_date' => $transfer['process_date'] ?? null, - 'encrypted' => 1, - 'order' => 0, - 'tag_count' => 0, - ] - ); - $amount = (rand($transfer['min_amount'] * 100, $transfer['max_amount'] * 100)) / 100; - $transactions[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'transaction_journal_id' => $journalId, - 'account_id' => $transfer['source_id'], - 'amount' => $amount * -1, - ]; - $transactions[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'transaction_journal_id' => $journalId, - 'account_id' => $transfer['destination_id'], - 'amount' => $amount, - ]; - // link to category if set. - if (isset($transfer['category_id'])) { - DB::table('category_transaction_journal')->insert( - [ - 'category_id' => $transfer['category_id'], - 'transaction_journal_id' => $journalId, - - ] - ); - } - } - DB::table('transactions')->insert($transactions); - $transactions = []; - $current->addMonth(); - } - - - } - - /** - * - */ - private function createMultiDeposits() - { - foreach ($this->data['multi-deposits'] as $deposit) { - $journalId = DB::table('transaction_journals')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $deposit['user_id'], - 'transaction_type_id' => 2, - 'transaction_currency_id' => 1, - 'description' => Crypt::encrypt($deposit['description']), - 'completed' => 1, - 'date' => $deposit['date'], - 'interest_date' => $deposit['interest_date'] ?? null, - 'book_date' => $deposit['book_date'] ?? null, - 'process_date' => $deposit['process_date'] ?? null, - 'encrypted' => 1, - 'order' => 0, - 'tag_count' => 0, - ] - ); - $identifier = 0; - foreach ($deposit['source_ids'] as $index => $source) { - $description = $deposit['description'] . ' (#' . ($index + 1) . ')'; - $amount = $deposit['amounts'][$index]; - $first = DB::table('transactions')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'account_id' => $deposit['destination_id'], - 'transaction_journal_id' => $journalId, - 'description' => $description, - 'amount' => $amount, - 'identifier' => $identifier, - ] - ); - $second = DB::table('transactions')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'account_id' => $source, - 'transaction_journal_id' => $journalId, - 'description' => $description, - 'amount' => $amount * -1, - 'identifier' => $identifier, - ] - ); - $identifier++; - // link first and second to budget and category, if present. - - if (isset($deposit['category_ids'][$index])) { - DB::table('category_transaction')->insert( - [ - 'category_id' => $deposit['category_ids'][$index], - 'transaction_id' => $first, - ] - ); - DB::table('category_transaction')->insert( - [ - 'category_id' => $deposit['category_ids'][$index], - 'transaction_id' => $second, - ] - ); - } - } - } - } - - private function createMultiTransfers() - { - foreach ($this->data['multi-transfers'] as $transfer) { - $journalId = DB::table('transaction_journals')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $transfer['user_id'], - 'transaction_type_id' => 3, - 'transaction_currency_id' => 1, - 'description' => Crypt::encrypt($transfer['description']), - 'completed' => 1, - 'date' => $transfer['date'], - 'interest_date' => $transfer['interest_date'] ?? null, - 'book_date' => $transfer['book_date'] ?? null, - 'process_date' => $transfer['process_date'] ?? null, - 'encrypted' => 1, - 'order' => 0, - 'tag_count' => 0, - ] - ); - $identifier = 0; - foreach ($transfer['destination_ids'] as $index => $destination) { - $description = $transfer['description'] . ' (#' . ($index + 1) . ')'; - $amount = $transfer['amounts'][$index]; - $source = $transfer['source_ids'][$index]; - $first = DB::table('transactions')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'account_id' => $source, - 'transaction_journal_id' => $journalId, - 'description' => $description, - 'amount' => $amount * -1, - 'identifier' => $identifier, - ] - ); - $second = DB::table('transactions')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'account_id' => $destination, - 'transaction_journal_id' => $journalId, - 'description' => $description, - 'amount' => $amount, - 'identifier' => $identifier, - ] - ); - $identifier++; - if (isset($transfer['category_ids'][$index])) { - DB::table('category_transaction')->insert( - [ - 'category_id' => $transfer['category_ids'][$index], - 'transaction_id' => $first, - ] - ); - DB::table('category_transaction')->insert( - [ - 'category_id' => $transfer['category_ids'][$index], - 'transaction_id' => $second, - ] - ); - } - } - } - } - - /** - * - */ - private function createMultiWithdrawals() - { - foreach ($this->data['multi-withdrawals'] as $withdrawal) { - $journalId = DB::table('transaction_journals')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $withdrawal['user_id'], - 'transaction_type_id' => 1, - 'transaction_currency_id' => 1, - 'description' => Crypt::encrypt($withdrawal['description']), - 'completed' => 1, - 'date' => $withdrawal['date'], - 'interest_date' => $withdrawal['interest_date'] ?? null, - 'book_date' => $withdrawal['book_date'] ?? null, - 'process_date' => $withdrawal['process_date'] ?? null, - 'encrypted' => 1, - 'order' => 0, - 'tag_count' => 0, - ] - ); - $identifier = 0; - foreach ($withdrawal['destination_ids'] as $index => $destination) { - $description = $withdrawal['description'] . ' (#' . ($index + 1) . ')'; - $amount = $withdrawal['amounts'][$index]; - $first = DB::table('transactions')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'account_id' => $withdrawal['source_id'], - 'transaction_journal_id' => $journalId, - 'description' => $description, - 'amount' => $amount * -1, - 'identifier' => $identifier, - ] - ); - $second = DB::table('transactions')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'account_id' => $destination, - 'transaction_journal_id' => $journalId, - 'description' => $description, - 'amount' => $amount, - 'identifier' => $identifier, - ] - ); - $identifier++; - // link first and second to budget and category, if present. - if (isset($withdrawal['budget_ids'][$index])) { - DB::table('budget_transaction')->insert( - [ - 'budget_id' => $withdrawal['budget_ids'][$index], - 'transaction_id' => $first, - ] - ); - DB::table('budget_transaction')->insert( - [ - 'budget_id' => $withdrawal['budget_ids'][$index], - 'transaction_id' => $second, - ] - ); - } - - if (isset($withdrawal['category_ids'][$index])) { - DB::table('category_transaction')->insert( - [ - 'category_id' => $withdrawal['category_ids'][$index], - 'transaction_id' => $first, - ] - ); - DB::table('category_transaction')->insert( - [ - 'category_id' => $withdrawal['category_ids'][$index], - 'transaction_id' => $second, - ] - ); - } - } - } - } - - /** - * - */ - private function createPiggyBanks() - { - foreach ($this->data['piggy-banks'] as $piggyBank) { - $piggyId = DB::table('piggy_banks')->insertGetId( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'account_id' => $piggyBank['account_id'], - 'name' => Crypt::encrypt($piggyBank['name']), - 'targetamount' => $piggyBank['targetamount'], - 'startdate' => $piggyBank['startdate'], - 'order' => $piggyBank['order'], - 'encrypted' => 1, - ] - ); - if (isset($piggyBank['currentamount'])) { - DB::table('piggy_bank_repetitions')->insert( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'piggy_bank_id' => $piggyId, - 'startdate' => $piggyBank['startdate'], - 'currentamount' => $piggyBank['currentamount'], - ] - ); - } - } - - foreach ($this->data['piggy-events'] as $event) { - DB::table('piggy_bank_events')->insert( - [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'piggy_bank_id' => $event['piggy_bank_id'], - 'date' => $event['date'], - 'amount' => $event['amount'], - ] - ); - } - } - - /** - * - */ - private function createRules() - { - $insert = []; - foreach ($this->data['rule-groups'] as $group) { - $insert[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $group['user_id'], - 'order' => $group['order'], - 'title' => $group['title'], - 'description' => $group['description'], - 'active' => 1, - ]; - } - DB::table('rule_groups')->insert($insert); - $insert = []; - foreach ($this->data['rules'] as $rule) { - $insert[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $rule['user_id'], - 'rule_group_id' => $rule['rule_group_id'], - 'order' => $rule['order'], - 'active' => $rule['active'], - 'stop_processing' => $rule['stop_processing'], - 'title' => $rule['title'], - 'description' => $rule['description'], - ]; - } - DB::table('rules')->insert($insert); - - $insert = []; - foreach ($this->data['rule-triggers'] as $trigger) { - $insert[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'rule_id' => $trigger['rule_id'], - 'order' => $trigger['order'], - 'active' => $trigger['active'], - 'stop_processing' => $trigger['stop_processing'], - 'trigger_type' => $trigger['trigger_type'], - 'trigger_value' => $trigger['trigger_value'], - ]; - } - DB::table('rule_triggers')->insert($insert); - - $insert = []; - foreach ($this->data['rule-actions'] as $action) { - $insert[] = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'rule_id' => $action['rule_id'], - 'order' => $action['order'], - 'active' => $action['active'], - 'stop_processing' => $action['stop_processing'], - 'action_type' => $action['action_type'], - 'action_value' => $action['action_value'], - ]; - } - DB::table('rule_actions')->insert($insert); - } - - /** - * - */ - private function createTags() - { - $insert = []; - foreach ($this->data['tags'] as $tag) { - $insert[] - = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'user_id' => $tag['user_id'], - 'tag' => Crypt::encrypt($tag['tag']), - 'tagMode' => $tag['tagMode'], - 'date' => $tag['date'] ?? null, - ]; - - } - DB::table('tags')->insert($insert); - } - - /** - * - */ - private function createUsers() - { - $insert = []; - foreach ($this->data['users'] as $user) { - $insert[] - = [ - 'created_at' => $this->time, - 'updated_at' => $this->time, - 'email' => $user['email'], - 'remember_token' => '', - 'password' => bcrypt($user['password']), - ]; - - } - DB::table('users')->insert($insert); - $insert = []; - foreach ($this->data['roles'] as $role) { - $insert[] - = [ - 'user_id' => $role['user_id'], - 'role_id' => $role['role'], - ]; - } - DB::table('role_user')->insert($insert); - } - - /** - * - */ - private function go() - { - $this->createUsers(); - $this->createAccounts(); - $this->createBills(); - $this->createBudgets(); - $this->createCategories(); - $this->createPiggyBanks(); - $this->createRules(); - $this->createTags(); - $this->createJournals(); - $this->createAttachments(); - $this->createMultiWithdrawals(); - $this->createMultiDeposits(); - $this->createMultiTransfers(); - $this->createImportJobs(); - $this->createCurrencies(); - } - -} diff --git a/app/Support/Models/TransactionJournalSupport.php b/app/Support/Models/TransactionJournalSupport.php index 49901f7119..513fec2623 100644 --- a/app/Support/Models/TransactionJournalSupport.php +++ b/app/Support/Models/TransactionJournalSupport.php @@ -200,7 +200,7 @@ class TransactionJournalSupport extends Model * * @return bool */ - public static function isJoined(Builder $query, string $table):bool + public static function isJoined(Builder $query, string $table): bool { $joins = $query->getQuery()->joins; if (is_null($joins)) { diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index 4ea2318acb..cbdeafbabc 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -24,7 +24,6 @@ use FireflyIII\Exceptions\FireflyException; class Navigation { - /** * @param \Carbon\Carbon $theDate * @param $repeatFreq @@ -44,6 +43,7 @@ class Navigation '1M' => 'addMonths', 'month' => 'addMonths', 'monthly' => 'addMonths', '3M' => 'addMonths', 'quarter' => 'addMonths', 'quarterly' => 'addMonths', '6M' => 'addMonths', 'half-year' => 'addMonths', 'year' => 'addYears', 'yearly' => 'addYears', '1Y' => 'addYears', + 'custom' => 'addMonths', // custom? just add one month. ]; $modifierMap = [ 'quarter' => 3, @@ -54,7 +54,7 @@ class Navigation ]; if (!isset($functionMap[$repeatFreq])) { - throw new FireflyException('Cannot do addPeriod for $repeat_freq "' . $repeatFreq . '"'); + throw new FireflyException(sprintf('Cannot do addPeriod for $repeat_freq "%s"', $repeatFreq)); } if (isset($modifierMap[$repeatFreq])) { $add = $add * $modifierMap[$repeatFreq]; @@ -108,7 +108,7 @@ class Navigation } if (!isset($functionMap[$repeatFreq])) { - throw new FireflyException('Cannot do endOfPeriod for $repeat_freq "' . $repeatFreq . '"'); + throw new FireflyException(sprintf('Cannot do endOfPeriod for $repeat_freq "%s"', $repeatFreq)); } $function = $functionMap[$repeatFreq]; if (isset($modifierMap[$repeatFreq])) { @@ -129,12 +129,11 @@ class Navigation } /** + * @param \Carbon\Carbon $theCurrentEnd + * @param string $repeatFreq + * @param \Carbon\Carbon|null $maxDate * - * @param \Carbon\Carbon $theCurrentEnd - * @param $repeatFreq - * @param \Carbon\Carbon $maxDate - * - * @return \Carbon\Carbon + * @return Carbon */ public function endOfX(Carbon $theCurrentEnd, string $repeatFreq, Carbon $maxDate = null): Carbon { @@ -170,6 +169,44 @@ class Navigation return $currentEnd; } + /** + * @param \Carbon\Carbon $start + * @param \Carbon\Carbon $end + * + * @return array + */ + public function listOfPeriods(Carbon $start, Carbon $end): array + { + // define period to increment + $increment = 'addDay'; + $format = self::preferredCarbonFormat($start, $end); + $displayFormat = strval(trans('config.month_and_day')); + // increment by month (for year) + if ($start->diffInMonths($end) > 1) { + $increment = 'addMonth'; + $displayFormat = strval(trans('config.month')); + } + + // increment by year (for multi year) + if ($start->diffInMonths($end) > 12) { + $increment = 'addYear'; + $displayFormat = strval(trans('config.year')); + } + + $begin = clone $start; + $entries = []; + while ($begin < $end) { + $formatted = $begin->format($format); + $displayed = $begin->formatLocalized($displayFormat); + $entries[$formatted] = $displayed; + + $begin->$increment(); + } + + return $entries; + + } + /** * @param \Carbon\Carbon $date * @param $repeatFrequency @@ -202,7 +239,125 @@ class Navigation if (isset($formatMap[$repeatFrequency])) { return $date->formatLocalized(strval($formatMap[$repeatFrequency])); } - throw new FireflyException('No date formats for frequency "' . $repeatFrequency . '"!'); + throw new FireflyException(sprintf('No date formats for frequency "%s"!', $repeatFrequency)); + } + + /** + * If the date difference between start and end is less than a month, method returns "Y-m-d". If the difference is less than a year, + * method returns "Y-m". If the date difference is larger, method returns "Y". + * + * @param \Carbon\Carbon $start + * @param \Carbon\Carbon $end + * + * @return string + */ + public function preferredCarbonFormat(Carbon $start, Carbon $end): string + { + $format = 'Y-m-d'; + if ($start->diffInMonths($end) > 1) { + $format = 'Y-m'; + } + + if ($start->diffInMonths($end) > 12) { + $format = 'Y'; + } + + return $format; + } + + /** + * If the date difference between start and end is less than a month, method returns trans(config.month_and_day). If the difference is less than a year, + * method returns "config.month". If the date difference is larger, method returns "config.year". + * + * @param \Carbon\Carbon $start + * @param \Carbon\Carbon $end + * + * @return string + */ + public function preferredCarbonLocalizedFormat(Carbon $start, Carbon $end): string + { + $format = strval(trans('config.month_and_day')); + if ($start->diffInMonths($end) > 1) { + $format = strval(trans('config.month')); + } + + if ($start->diffInMonths($end) > 12) { + $format = strval(trans('config.year')); + } + + return $format; + + } + + /** + * If the date difference between start and end is less than a month, method returns "endOfDay". If the difference is less than a year, + * method returns "endOfMonth". If the date difference is larger, method returns "endOfYear". + * + * @param \Carbon\Carbon $start + * @param \Carbon\Carbon $end + * + * @return string + */ + public function preferredEndOfPeriod(Carbon $start, Carbon $end): string + { + $format = 'endOfDay'; + if ($start->diffInMonths($end) > 1) { + $format = 'endOfMonth'; + } + + if ($start->diffInMonths($end) > 12) { + $format = 'endOfYear'; + } + + return $format; + } + + /** + * If the date difference between start and end is less than a month, method returns "1D". If the difference is less than a year, + * method returns "1M". If the date difference is larger, method returns "1Y". + * + * @param \Carbon\Carbon $start + * @param \Carbon\Carbon $end + * + * @return string + */ + public function preferredRangeFormat(Carbon $start, Carbon $end): string + { + $format = '1D'; + if ($start->diffInMonths($end) > 1) { + $format = '1M'; + } + + if ($start->diffInMonths($end) > 12) { + $format = '1Y'; + } + + return $format; + + } + + /** + * If the date difference between start and end is less than a month, method returns "%Y-%m-%d". If the difference is less than a year, + * method returns "%Y-%m". If the date difference is larger, method returns "%Y". + * + * @param \Carbon\Carbon $start + * @param \Carbon\Carbon $end + * + * @return string + */ + public function preferredSqlFormat(Carbon $start, Carbon $end): string + { + $format = '%Y-%m-%d'; + if ($start->diffInMonths($end) > 1) { + $format = '%Y-%m'; + } + + if ($start->diffInMonths($end) > 12) { + $format = '%Y'; + } + + return $format; + } /** @@ -252,7 +407,7 @@ class Navigation } - throw new FireflyException('Cannot do startOfPeriod for $repeat_freq "' . $repeatFreq . '"'); + throw new FireflyException(sprintf('Cannot do startOfPeriod for $repeat_freq "%s"', $repeatFreq)); } /** @@ -313,7 +468,7 @@ class Navigation return $date; } - throw new FireflyException('Cannot do subtractPeriod for $repeat_freq "' . $repeatFreq . '"'); + throw new FireflyException(sprintf('Cannot do subtractPeriod for $repeat_freq "%s"', $repeatFreq)); } /** @@ -326,11 +481,12 @@ class Navigation public function updateEndDate(string $range, Carbon $start): Carbon { $functionMap = [ - '1D' => 'endOfDay', - '1W' => 'endOfWeek', - '1M' => 'endOfMonth', - '3M' => 'lastOfQuarter', - '1Y' => 'endOfYear', + '1D' => 'endOfDay', + '1W' => 'endOfWeek', + '1M' => 'endOfMonth', + '3M' => 'lastOfQuarter', + '1Y' => 'endOfYear', + 'custom' => 'startOfMonth', // this only happens in test situations. ]; $end = clone $start; @@ -350,7 +506,7 @@ class Navigation return $end; } - throw new FireflyException('updateEndDate cannot handle $range "' . $range . '"'); + throw new FireflyException(sprintf('updateEndDate cannot handle range "%s"', $range)); } /** @@ -363,11 +519,12 @@ class Navigation public function updateStartDate(string $range, Carbon $start): Carbon { $functionMap = [ - '1D' => 'startOfDay', - '1W' => 'startOfWeek', - '1M' => 'startOfMonth', - '3M' => 'firstOfQuarter', - '1Y' => 'startOfYear', + '1D' => 'startOfDay', + '1W' => 'startOfWeek', + '1M' => 'startOfMonth', + '3M' => 'firstOfQuarter', + '1Y' => 'startOfYear', + 'custom' => 'startOfMonth', // this only happens in test situations. ]; if (isset($functionMap[$range])) { $function = $functionMap[$range]; @@ -387,7 +544,7 @@ class Navigation } - throw new FireflyException('updateStartDate cannot handle $range "' . $range . '"'); + throw new FireflyException(sprintf('updateStartDate cannot handle range "%s"', $range)); } diff --git a/app/Support/Preferences.php b/app/Support/Preferences.php index 8471745dbc..999b8847ef 100644 --- a/app/Support/Preferences.php +++ b/app/Support/Preferences.php @@ -16,6 +16,7 @@ namespace FireflyIII\Support; use Cache; use FireflyIII\Models\Preference; use FireflyIII\User; +use Session; /** * Class Preferences @@ -32,7 +33,7 @@ class Preferences */ public function delete($name): bool { - $fullName = 'preference' . auth()->user()->id . $name; + $fullName = sprintf('preference%s%s', auth()->user()->id, $name); if (Cache::has($fullName)) { Cache::forget($fullName); } @@ -57,16 +58,40 @@ class Preferences return $this->getForUser(auth()->user(), $name, $default); } + /** + * @param \FireflyIII\User $user + * @param array $list + * + * @return array + */ + public function getArrayForUser(User $user, array $list): array + { + $result = []; + $preferences = Preference::where('user_id', $user->id)->whereIn('name', $list)->get(['id', 'name', 'data']); + /** @var Preference $preference */ + foreach ($preferences as $preference) { + $result[$preference->name] = $preference->data; + } + foreach ($list as $name) { + if (!isset($result[$name])) { + $result[$name] = null; + } + } + + return $result; + + } + /** * @param \FireflyIII\User $user * @param string $name - * @param string $default + * @param null|string $default * * @return \FireflyIII\Models\Preference|null */ public function getForUser(User $user, $name, $default = null) { - $fullName = 'preference' . $user->id . $name; + $fullName = sprintf('preference%s%s', $user->id, $name); if (Cache::has($fullName)) { return Cache::get($fullName); } @@ -104,6 +129,7 @@ class Preferences public function mark(): bool { $this->set('lastActivity', microtime()); + Session::forget('first'); return true; } @@ -138,7 +164,7 @@ class Preferences */ public function setForUser(User $user, $name, $value): Preference { - $fullName = 'preference' . $user->id . $name; + $fullName = sprintf('preference%s%s', $user->id, $name); Cache::forget($fullName); $pref = Preference::where('user_id', $user->id)->where('name', $name)->first(['id', 'name', 'data']); diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index d20e83413f..b37af85daa 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -14,11 +14,16 @@ declare(strict_types = 1); namespace FireflyIII\Support\Search; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; use FireflyIII\Models\Budget; use FireflyIII\Models\Category; -use FireflyIII\Models\TransactionJournal; -use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use FireflyIII\Models\Tag; +use FireflyIII\Models\Transaction; +use FireflyIII\User; use Illuminate\Support\Collection; +use Log; /** * Class Search @@ -27,20 +32,38 @@ use Illuminate\Support\Collection; */ class Search implements SearchInterface { + /** @var int */ + private $limit = 100; + /** @var User */ + private $user; + /** + * The search will assume that the user does not have so many accounts + * that this search should be paginated. + * * @param array $words * * @return Collection */ public function searchAccounts(array $words): Collection { - return auth()->user()->accounts()->with('accounttype')->where( - function (EloquentBuilder $q) use ($words) { - foreach ($words as $word) { - $q->orWhere('name', 'LIKE', '%' . e($word) . '%'); + $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; } - )->get(); + ); + + $result = $result->slice(0, $this->limit); + + return $result; } /** @@ -51,46 +74,46 @@ class Search implements SearchInterface public function searchBudgets(array $words): Collection { /** @var Collection $set */ - $set = auth()->user()->budgets()->get(); - $newSet = $set->filter( - function (Budget $b) use ($words) { - $found = 0; - foreach ($words as $word) { - if (!(strpos(strtolower($b->name), strtolower($word)) === false)) { - $found++; - } + $set = auth()->user()->budgets()->get(); + /** @var Collection $result */ + $result = $set->filter( + function (Budget $budget) use ($words) { + if ($this->strpos_arr(strtolower($budget->name), $words)) { + return $budget; } - return $found > 0; + return false; } ); - return $newSet; + $result = $result->slice(0, $this->limit); + + return $result; } /** + * Search assumes the user does not have that many categories. So no paginated search. + * * @param array $words * * @return Collection */ public function searchCategories(array $words): Collection { - /** @var Collection $set */ - $set = auth()->user()->categories()->get(); - $newSet = $set->filter( - function (Category $c) use ($words) { - $found = 0; - foreach ($words as $word) { - if (!(strpos(strtolower($c->name), strtolower($word)) === false)) { - $found++; - } + $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 $found > 0; + return false; } ); + $result = $result->slice(0, $this->limit); - return $newSet; + return $result; } /** @@ -101,7 +124,21 @@ class Search implements SearchInterface */ public function searchTags(array $words): Collection { - return new Collection; + $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; } /** @@ -111,40 +148,96 @@ class Search implements SearchInterface */ public function searchTransactions(array $words): Collection { - // decrypted transaction journals: - $decrypted = auth()->user()->transactionJournals()->expanded()->where('transaction_journals.encrypted', 0)->where( - function (EloquentBuilder $q) use ($words) { - foreach ($words as $word) { - $q->orWhere('transaction_journals.description', 'LIKE', '%' . e($word) . '%'); - } - } - )->get(TransactionJournal::queryFields()); + $pageSize = 100; + $processed = 0; + $page = 1; + $result = new Collection(); + do { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page); + $set = $collector->getPaginatedJournals(); + Log::debug(sprintf('Found %d journals to check. ', $set->count())); - // encrypted - $all = auth()->user()->transactionJournals()->expanded()->where('transaction_journals.encrypted', 1)->get(TransactionJournal::queryFields()); - $set = $all->filter( - function (TransactionJournal $journal) use ($words) { - foreach ($words as $word) { - $haystack = strtolower($journal->description); - $word = strtolower($word); - if (!(strpos($haystack, $word) === false)) { - return $journal; + // Filter transactions that match the given triggers. + $filtered = $set->filter( + function (Transaction $transaction) use ($words) { + // check descr of journal: + if ($this->strpos_arr(strtolower(strval($transaction->description)), $words)) { + return $transaction; } + + // check descr of transaction + if ($this->strpos_arr(strtolower(strval($transaction->transaction_description)), $words)) { + return $transaction; + } + + // return false: + return false; } + ); - return null; + Log::debug(sprintf('Found %d journals that match.', $filtered->count())); - } - ); - $filtered = $set->merge($decrypted); - $filtered = $filtered->sortBy( - function (TransactionJournal $journal) { - return intval($journal->date->format('U')); - } - ); + // merge: + /** @var Collection $result */ + $result = $result->merge($filtered); + Log::debug(sprintf('Total count is now %d', $result->count())); - $filtered = $filtered->reverse(); + // Update counters + $page++; + $processed += count($set); - return $filtered; + Log::debug(sprintf('Page is now %d, processed is %d', $page, $processed)); + + // Check for conditions to finish the loop + $reachedEndOfList = $set->count() < 1; + $foundEnough = $result->count() >= $this->limit; + + Log::debug(sprintf('reachedEndOfList: %s', var_export($reachedEndOfList, true))); + Log::debug(sprintf('foundEnough: %s', var_export($foundEnough, true))); + + } while (!$reachedEndOfList && !$foundEnough); + + $result = $result->slice(0, $this->limit); + + return $result; } -} + + /** + * @param int $limit + */ + public function setLimit(int $limit) + { + $this->limit = $limit; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + + /** + * @param string $haystack + * @param array $needle + * + * @return bool + */ + private function strpos_arr(string $haystack, array $needle) + { + if (strlen($haystack) === 0) { + return false; + } + foreach ($needle as $what) { + if (($pos = strpos($haystack, $what)) !== false) { + return true; + } + } + + return false; + } +} diff --git a/app/Support/Search/SearchInterface.php b/app/Support/Search/SearchInterface.php index bd16c78853..ed9c3c6c88 100644 --- a/app/Support/Search/SearchInterface.php +++ b/app/Support/Search/SearchInterface.php @@ -13,6 +13,7 @@ declare(strict_types = 1); namespace FireflyIII\Support\Search; +use FireflyIII\User; use Illuminate\Support\Collection; /** @@ -29,6 +30,11 @@ interface SearchInterface */ public function searchAccounts(array $words): Collection; + /** + * @param User $user + */ + public function setUser(User $user); + /** * @param array $words * diff --git a/app/Support/Steam.php b/app/Support/Steam.php index d94b7af230..870b55566c 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -26,7 +26,6 @@ use FireflyIII\Models\Transaction; class Steam { - /** * * @param \FireflyIII\Models\Account $account @@ -124,6 +123,8 @@ class Steam ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) ->groupBy('transaction_journals.date') + ->orderBy('transaction_journals.date', 'ASC') + ->whereNull('transaction_journals.deleted_at') ->get(['transaction_journals.date', DB::raw('SUM(transactions.amount) AS modified')]); $currentBalance = $startBalance; foreach ($set as $entry) { @@ -150,7 +151,7 @@ class Steam public function balancesById(array $ids, Carbon $date): array { - // abuse chart properties: + // cache this property. $cache = new CacheProperties; $cache->addProperty($ids); $cache->addProperty('balances'); @@ -159,11 +160,11 @@ class Steam return $cache->get(); } - $balances = Transaction:: - leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + $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')]); $result = []; @@ -200,8 +201,6 @@ class Steam return $list; } - // parse PHP size: - /** * @param $string * @@ -237,4 +236,20 @@ class Steam } + // parse PHP size: + + /** + * @param string $amount + * + * @return string + */ + public function positive(string $amount): string + { + if (bccomp($amount, '0') === -1) { + $amount = bcmul($amount, '-1'); + } + + return $amount; + } + } diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php index a13a62faab..5d339960fa 100644 --- a/app/Support/Twig/General.php +++ b/app/Support/Twig/General.php @@ -14,10 +14,10 @@ declare(strict_types = 1); namespace FireflyIII\Support\Twig; use Carbon\Carbon; -use Config; use FireflyIII\Models\Account; use FireflyIII\Models\TransactionJournal; use Route; +use Steam; use Twig_Extension; use Twig_SimpleFilter; use Twig_SimpleFunction; @@ -42,9 +42,9 @@ class General extends Twig_Extension $this->formatAmountPlain(), $this->formatJournal(), $this->balance(), - $this->getAccountRole(), $this->formatFilesize(), $this->mimeIcon(), + ]; } @@ -61,6 +61,7 @@ class General extends Twig_Extension $this->env(), $this->getAmountFromJournal(), $this->activeRouteStrict(), + $this->steamPositive(), $this->activeRoutePartial(), $this->activeRoutePartialWhat(), ]; @@ -84,7 +85,7 @@ class General extends Twig_Extension protected function activeRoutePartial(): Twig_SimpleFunction { return new Twig_SimpleFunction( - 'activeRoutePartial', function () : string { + 'activeRoutePartial', function (): string { $args = func_get_args(); $route = $args[0]; // name of the route. $name = Route::getCurrentRoute()->getName() ?? ''; @@ -106,7 +107,7 @@ class General extends Twig_Extension protected function activeRoutePartialWhat(): Twig_SimpleFunction { return new Twig_SimpleFunction( - 'activeRoutePartialWhat', function ($context) : string { + 'activeRoutePartialWhat', function ($context): string { $args = func_get_args(); $route = $args[1]; // name of the route. $what = $args[2]; // name of the route. @@ -130,7 +131,7 @@ class General extends Twig_Extension protected function activeRouteStrict(): Twig_SimpleFunction { return new Twig_SimpleFunction( - 'activeRouteStrict', function () : string { + 'activeRouteStrict', function (): string { $args = func_get_args(); $route = $args[0]; // name of the route. @@ -149,7 +150,7 @@ class General extends Twig_Extension protected function balance(): Twig_SimpleFilter { return new Twig_SimpleFilter( - 'balance', function (Account $account = null) : string { + 'balance', function (Account $account = null): string { if (is_null($account)) { return 'NULL'; } @@ -166,7 +167,7 @@ class General extends Twig_Extension protected function env(): Twig_SimpleFunction { return new Twig_SimpleFunction( - 'env', function (string $name, string $default) : string { + 'env', function (string $name, string $default): string { return env($name, $default); } ); @@ -179,7 +180,7 @@ class General extends Twig_Extension protected function formatAmount(): Twig_SimpleFilter { return new Twig_SimpleFilter( - 'formatAmount', function (string $string) : string { + 'formatAmount', function (string $string): string { return app('amount')->format($string); }, ['is_safe' => ['html']] @@ -192,7 +193,7 @@ class General extends Twig_Extension protected function formatAmountPlain(): Twig_SimpleFilter { return new Twig_SimpleFilter( - 'formatAmountPlain', function (string $string) : string { + 'formatAmountPlain', function (string $string): string { return app('amount')->format($string, false); }, ['is_safe' => ['html']] @@ -205,7 +206,7 @@ class General extends Twig_Extension protected function formatFilesize(): Twig_SimpleFilter { return new Twig_SimpleFilter( - 'filesize', function (int $size) : string { + 'filesize', function (int $size): string { // less than one GB, more than one MB if ($size < (1024 * 1024 * 2014) && $size >= (1024 * 1024)) { @@ -228,31 +229,19 @@ class General extends Twig_Extension protected function formatJournal(): Twig_SimpleFilter { return new Twig_SimpleFilter( - 'formatJournal', function (TransactionJournal $journal) : string { + 'formatJournal', function (TransactionJournal $journal): string { return app('amount')->formatJournal($journal); }, ['is_safe' => ['html']] ); } - /** - * @return Twig_SimpleFilter - */ - protected function getAccountRole(): Twig_SimpleFilter - { - return new Twig_SimpleFilter( - 'getAccountRole', function (string $name) : string { - return Config::get('firefly.accountRoles.' . $name); - } - ); - } - /** * @return Twig_SimpleFunction */ protected function getCurrencyCode(): Twig_SimpleFunction { return new Twig_SimpleFunction( - 'getCurrencyCode', function () : string { + 'getCurrencyCode', function (): string { return app('amount')->getCurrencyCode(); } ); @@ -264,7 +253,7 @@ class General extends Twig_Extension protected function getCurrencySymbol(): Twig_SimpleFunction { return new Twig_SimpleFunction( - 'getCurrencySymbol', function () : string { + 'getCurrencySymbol', function (): string { return app('amount')->getCurrencySymbol(); } ); @@ -276,7 +265,7 @@ class General extends Twig_Extension protected function mimeIcon(): Twig_SimpleFilter { return new Twig_SimpleFilter( - 'mimeIcon', function (string $string) : string { + 'mimeIcon', function (string $string): string { switch ($string) { default: return 'fa-file-o'; @@ -296,19 +285,31 @@ class General extends Twig_Extension protected function phpdate() { return new Twig_SimpleFunction( - 'phpdate', function (string $str) : string { + 'phpdate', function (string $str): string { return date($str); } ); } + /** + * @return Twig_SimpleFunction + */ + protected function steamPositive() + { + return new Twig_SimpleFunction( + 'steam_positive', function (string $str): string { + return Steam::positive($str); + } + ); + } + /** * @return Twig_SimpleFunction */ private function getAmountFromJournal() { return new Twig_SimpleFunction( - 'getAmount', function (TransactionJournal $journal) : string { + 'getAmount', function (TransactionJournal $journal): string { return TransactionJournal::amount($journal); } ); diff --git a/app/Support/Twig/Journal.php b/app/Support/Twig/Journal.php index 62f99d89a6..757264a240 100644 --- a/app/Support/Twig/Journal.php +++ b/app/Support/Twig/Journal.php @@ -14,9 +14,8 @@ declare(strict_types = 1); namespace FireflyIII\Support\Twig; -use Amount; use FireflyIII\Models\Account; -use FireflyIII\Models\Budget as ModelBudget; +use FireflyIII\Models\AccountType; use FireflyIII\Models\Category; use FireflyIII\Models\TransactionJournal; use FireflyIII\Support\CacheProperties; @@ -32,96 +31,6 @@ use Twig_SimpleFunction; class Journal extends Twig_Extension { - /** - * @return Twig_SimpleFunction - */ - public function formatAccountPerspective(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'formatAccountPerspective', function (TransactionJournal $journal, Account $account) { - - $cache = new CacheProperties; - $cache->addProperty('formatAccountPerspective'); - $cache->addProperty($journal->id); - $cache->addProperty($account->id); - - if ($cache->has()) { - return $cache->get(); - } - - // get the account amount: - $transactions = $journal->transactions()->where('transactions.account_id', $account->id)->get(['transactions.*']); - $amount = '0'; - foreach ($transactions as $transaction) { - $amount = bcadd($amount, strval($transaction->amount)); - } - if ($journal->isTransfer()) { - $amount = bcmul($amount, '-1'); - } - - // check if this sum is the same as the journal: - $journalSum = TransactionJournal::amount($journal); - $full = Amount::formatJournal($journal); - if (bccomp($journalSum, $amount) === 0 || bccomp(bcmul($journalSum, '-1'), $amount) === 0) { - $cache->store($full); - - return $full; - } - - $formatted = Amount::format($amount, true); - - if ($journal->isTransfer()) { - $formatted = '' . Amount::format($amount) . ''; - } - $str = $formatted . ' (' . $full . ')'; - $cache->store($str); - - return $str; - - } - ); - } - - /** - * @return Twig_SimpleFunction - */ - public function formatBudgetPerspective(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'formatBudgetPerspective', function (TransactionJournal $journal, ModelBudget $budget) { - - $cache = new CacheProperties; - $cache->addProperty('formatBudgetPerspective'); - $cache->addProperty($journal->id); - $cache->addProperty($budget->id); - - if ($cache->has()) { - return $cache->get(); - } - - // get the account amount: - $transactions = $journal->transactions()->where('transactions.amount', '<', 0)->get(['transactions.*']); - $amount = '0'; - foreach ($transactions as $transaction) { - $currentBudget = $transaction->budgets->first(); - if (!is_null($currentBudget) && $currentBudget->id === $budget->id) { - $amount = bcadd($amount, strval($transaction->amount)); - } - } - if ($amount === '0') { - $formatted = Amount::formatJournal($journal); - $cache->store($formatted); - - return $formatted; - } - - $formatted = Amount::format($amount, true) . ' (' . Amount::formatJournal($journal) . ')'; - $cache->store($formatted); - - return $formatted; - } - ); - } /** * @return Twig_SimpleFunction @@ -142,18 +51,17 @@ 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; } - $array[] = '' . e($entry->name) . ''; + $array[] = sprintf('%1$s', e($entry->name), route('accounts.show', $entry->id)); } $array = array_unique($array); $result = join(', ', $array); $cache->store($result); return $result; - } ); } @@ -178,8 +86,6 @@ class Journal extends Twig_Extension $functions = [ $this->getSourceAccount(), $this->getDestinationAccount(), - $this->formatAccountPerspective(), - $this->formatBudgetPerspective(), $this->journalBudgets(), $this->journalCategories(), ]; @@ -221,7 +127,7 @@ class Journal extends Twig_Extension $array[] = '(cash)'; continue; } - $array[] = '' . e($entry->name) . ''; + $array[] = sprintf('%1$s', e($entry->name), route('accounts.show', $entry->id)); } $array = array_unique($array); $result = join(', ', $array); @@ -253,12 +159,12 @@ class Journal extends Twig_Extension $budgets = []; // get all budgets: foreach ($journal->budgets as $budget) { - $budgets[] = '' . e($budget->name) . ''; + $budgets[] = sprintf('%1$s', e($budget->name), route('budgets.show', $budget->id)); } // and more! foreach ($journal->transactions as $transaction) { foreach ($transaction->budgets as $budget) { - $budgets[] = '' . e($budget->name) . ''; + $budgets[] = sprintf('%1$s', e($budget->name), route('budgets.show', $budget->id)); } } $string = join(', ', array_unique($budgets)); @@ -288,7 +194,7 @@ class Journal extends Twig_Extension $categories = []; // get all categories for the journal itself (easy): foreach ($journal->categories as $category) { - $categories[] = '' . e($category->name) . ''; + $categories[] = sprintf('%1$s', e($category->name), route('categories.show', $category->id)); } if (count($categories) === 0) { $set = Category::distinct()->leftJoin('category_transaction', 'categories.id', '=', 'category_transaction.category_id') @@ -299,8 +205,7 @@ class Journal extends Twig_Extension ->get(['categories.*']); /** @var Category $category */ foreach ($set as $category) { - $categories[] = '' . e($category->name) - . ''; + $categories[] = sprintf('%1$s', e($category->name), route('categories.show', $category->id)); } } @@ -324,16 +229,16 @@ class Journal extends Twig_Extension switch (true) { case $journal->isWithdrawal(): - $txt = ''; + $txt = sprintf('', trans('firefly.withdrawal')); break; case $journal->isDeposit(): - $txt = ''; + $txt = sprintf('', trans('firefly.deposit')); break; case $journal->isTransfer(): - $txt = ''; + $txt = sprintf('', trans('firefly.transfer')); break; case $journal->isOpeningBalance(): - $txt = ''; + $txt = sprintf('', trans('firefly.openingBalance')); break; default: $txt = ''; diff --git a/app/Support/Twig/Transaction.php b/app/Support/Twig/Transaction.php index ceb5e7695b..5285a8ee90 100644 --- a/app/Support/Twig/Transaction.php +++ b/app/Support/Twig/Transaction.php @@ -17,6 +17,7 @@ use Amount; use Crypt; use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction as TransactionModel; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; use Twig_Extension; use Twig_SimpleFilter; @@ -29,15 +30,16 @@ use Twig_SimpleFunction; */ class Transaction extends Twig_Extension { + /** * @return Twig_SimpleFunction */ - public function formatAmountPlainWithCode(): Twig_SimpleFunction + public function formatAnything(): Twig_SimpleFunction { return new Twig_SimpleFunction( - 'formatAmountPlainWithCode', function (string $amount, string $code): string { + 'formatAnything', function (TransactionCurrency $currency, string $amount): string { - return Amount::formatWithCode($code, $amount, false); + return Amount::formatAnything($currency, $amount, true); }, ['is_safe' => ['html']] ); @@ -46,12 +48,26 @@ class Transaction extends Twig_Extension /** * @return Twig_SimpleFunction */ - public function formatAmountWithCode(): Twig_SimpleFunction + public function formatAnythingPlain(): Twig_SimpleFunction { return new Twig_SimpleFunction( - 'formatAmountWithCode', function (string $amount, string $code): string { + 'formatAnythingPlain', function (TransactionCurrency $currency, string $amount): string { - return Amount::formatWithCode($code, $amount, true); + 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']] ); @@ -75,8 +91,8 @@ class Transaction extends Twig_Extension public function getFunctions(): array { $functions = [ - $this->formatAmountWithCode(), - $this->formatAmountPlainWithCode(), + $this->formatAnything(), + $this->formatAnythingPlain(), $this->transactionSourceAccount(), $this->transactionDestinationAccount(), $this->optionalJournalAmount(), @@ -85,6 +101,7 @@ class Transaction extends Twig_Extension $this->transactionCategories(), $this->transactionIdCategories(), $this->splitJournalIndicator(), + $this->formatByCode(), ]; return $functions; @@ -107,25 +124,17 @@ class Transaction extends Twig_Extension { return new Twig_SimpleFunction( 'optionalJournalAmount', function (int $journalId, string $transactionAmount, string $code, string $type): string { - - $amount = strval( - TransactionModel - ::where('transaction_journal_id', $journalId) - ->whereNull('deleted_at') - ->where('amount', '<', 0) - ->sum('amount') - ); - + // 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'); } - if ( - bccomp($amount, $transactionAmount) !== 0 - && bccomp($amount, bcmul($transactionAmount, '-1')) !== 0 - ) { - // not equal? - return ' (' . Amount::formatWithCode($code, $amount, true) . ')'; + // 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 ''; @@ -144,7 +153,7 @@ class Transaction extends Twig_Extension 'splitJournalIndicator', function (int $journalId) { $count = TransactionModel::where('transaction_journal_id', $journalId)->whereNull('deleted_at')->count(); if ($count > 2) { - return ''; + return ''; } return ''; @@ -189,18 +198,25 @@ class Transaction extends Twig_Extension $name = intval($transaction->account_encrypted) === 1 ? Crypt::decrypt($transaction->account_name) : $transaction->account_name; $id = intval($transaction->account_id); $type = $transaction->account_type; - // if the amount is positive, assume that the current account (the one in $transaction) is indeed the destination account. - if (bccomp($transaction->transaction_amount, '0') === -1) { + // name is present in object, use that one: + if (bccomp($transaction->transaction_amount, '0') === -1 && !is_null($transaction->opposing_account_id)) { + + $name = $transaction->opposing_account_name; + $id = intval($transaction->opposing_account_id); + $type = intval($transaction->opposing_account_type); + } + + // Find the opposing account and use that one: + if (bccomp($transaction->transaction_amount, '0') === -1 && is_null($transaction->opposing_account_id)) { // if the amount is negative, find the opposing account and use that one: $journalId = $transaction->journal_id; /** @var TransactionModel $other */ - $other = TransactionModel - ::where('transaction_journal_id', $journalId)->where('transactions.id', '!=', $transaction->id) - ->where('amount', '=', bcmul($transaction->transaction_amount, '-1'))->where('identifier', $transaction->identifier) - ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->first(['transactions.account_id', 'accounts.encrypted', 'accounts.name', 'account_types.type']); + $other = TransactionModel::where('transaction_journal_id', $journalId)->where('transactions.id', '!=', $transaction->id) + ->where('amount', '=', bcmul($transaction->transaction_amount, '-1'))->where('identifier', $transaction->identifier) + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->first(['transactions.account_id', 'accounts.encrypted', 'accounts.name', 'account_types.type']); $name = intval($other->encrypted) === 1 ? Crypt::decrypt($other->name) : $other->name; $id = $other->account_id; $type = $other->type; @@ -210,7 +226,7 @@ class Transaction extends Twig_Extension return '(cash)'; } - return '' . e($name) . ''; + return sprintf('%1$s', e($name), route('accounts.show', [$id])); }, ['is_safe' => ['html']] ); @@ -252,21 +268,27 @@ class Transaction extends Twig_Extension return new Twig_SimpleFunction( 'transactionSourceAccount', function (TransactionModel $transaction): string { + // if the amount is negative, assume that the current account (the one in $transaction) is indeed the source account. $name = intval($transaction->account_encrypted) === 1 ? Crypt::decrypt($transaction->account_name) : $transaction->account_name; $id = intval($transaction->account_id); $type = $transaction->account_type; - // if the amount is negative, assume that the current account (the one in $transaction) is indeed the source account. - if (bccomp($transaction->transaction_amount, '0') === 1) { - // if the amount is positive, find the opposing account and use that one: + // name is present in object, use that one: + if (bccomp($transaction->transaction_amount, '0') === 1 && !is_null($transaction->opposing_account_id)) { + + $name = $transaction->opposing_account_name; + $id = intval($transaction->opposing_account_id); + $type = intval($transaction->opposing_account_type); + } + // Find the opposing account and use that one: + if (bccomp($transaction->transaction_amount, '0') === 1 && is_null($transaction->opposing_account_id)) { $journalId = $transaction->journal_id; /** @var TransactionModel $other */ - $other = TransactionModel - ::where('transaction_journal_id', $journalId)->where('transactions.id', '!=', $transaction->id) - ->where('amount', '=', bcmul($transaction->transaction_amount, '-1'))->where('identifier', $transaction->identifier) - ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->first(['transactions.account_id', 'accounts.encrypted', 'accounts.name', 'account_types.type']); + $other = TransactionModel::where('transaction_journal_id', $journalId)->where('transactions.id', '!=', $transaction->id) + ->where('amount', '=', bcmul($transaction->transaction_amount, '-1'))->where('identifier', $transaction->identifier) + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->first(['transactions.account_id', 'accounts.encrypted', 'accounts.name', 'account_types.type']); $name = intval($other->encrypted) === 1 ? Crypt::decrypt($other->name) : $other->name; $id = $other->account_id; $type = $other->type; @@ -276,7 +298,7 @@ class Transaction extends Twig_Extension return '(cash)'; } - return '' . e($name) . ''; + return sprintf('%1$s', e($name), route('accounts.show', [$id])); }, ['is_safe' => ['html']] ); @@ -294,16 +316,16 @@ class Transaction extends Twig_Extension switch ($transaction->transaction_type_type) { case TransactionType::WITHDRAWAL: - $txt = ''; + $txt = sprintf('', trans('firefly.withdrawal')); break; case TransactionType::DEPOSIT: - $txt = ''; + $txt = sprintf('', trans('firefly.deposit')); break; case TransactionType::TRANSFER: - $txt = ''; + $txt = sprintf('', trans('firefly.transfer')); break; case TransactionType::OPENING_BALANCE: - $txt = ''; + $txt = sprintf('', trans('firefly.openingBalance')); break; default: $txt = ''; @@ -315,6 +337,21 @@ class Transaction extends Twig_Extension ); } + /** + * @param int $isEncrypted + * @param string $value + * + * @return string + */ + private function encrypted(int $isEncrypted, string $value): string + { + if ($isEncrypted === 1) { + return Crypt::decrypt($value); + } + + return $value; + } + /** * @param TransactionModel $transaction * @@ -322,6 +359,20 @@ class Transaction extends Twig_Extension */ private function getTransactionBudgets(TransactionModel $transaction): string { + // journal has a budget: + if (isset($transaction->transaction_journal_budget_id)) { + $name = $this->encrypted(intval($transaction->transaction_journal_budget_encrypted), $transaction->transaction_journal_budget_name); + + return sprintf('%s', route('budgets.show', [$transaction->transaction_journal_budget_id]), $name, $name); + } + + // transaction has a budget + if (isset($transaction->transaction_budget_id)) { + $name = $this->encrypted(intval($transaction->transaction_budget_encrypted), $transaction->transaction_budget_name); + + return sprintf('%s', route('budgets.show', [$transaction->transaction_budget_id]), $name, $name); + } + // see if the transaction has a budget: $budgets = $transaction->budgets()->get(); if ($budgets->count() === 0) { @@ -347,6 +398,20 @@ class Transaction extends Twig_Extension */ private function getTransactionCategories(TransactionModel $transaction): string { + // journal has a category: + if (isset($transaction->transaction_journal_category_id)) { + $name = $this->encrypted(intval($transaction->transaction_journal_category_encrypted), $transaction->transaction_journal_category_name); + + return sprintf('%s', route('categories.show', [$transaction->transaction_journal_category_id]), $name, $name); + } + + // transaction has a category: + if (isset($transaction->transaction_category_id)) { + $name = $this->encrypted(intval($transaction->transaction_category_encrypted), $transaction->transaction_category_name); + + return sprintf('%s', route('categories.show', [$transaction->transaction_category_id]), $name, $name); + } + // see if the transaction has a category: $categories = $transaction->categories()->get(); if ($categories->count() === 0) { diff --git a/app/Support/Twig/Translation.php b/app/Support/Twig/Translation.php index f9401eee52..cb84f85c8e 100644 --- a/app/Support/Twig/Translation.php +++ b/app/Support/Twig/Translation.php @@ -35,7 +35,7 @@ class Translation extends Twig_Extension $filters[] = new Twig_SimpleFilter( '_', function ($name) { - return trans('firefly.' . $name); + return strval(trans(sprintf('firefly.%s', $name))); }, ['is_safe' => ['html']] ); diff --git a/app/User.php b/app/User.php old mode 100755 new mode 100644 index 72ccc2f8b8..06a150ef49 --- a/app/User.php +++ b/app/User.php @@ -14,52 +14,18 @@ declare(strict_types = 1); namespace FireflyIII; +use FireflyIII\Events\RequestedNewPassword; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Request; /** * Class User * * @package FireflyIII - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property string $email - * @property string $password - * @property string $remember_token - * @property string $reset - * @property boolean $blocked - * @property string $blocked_code - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Account[] $accounts - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Attachment[] $attachments - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Bill[] $bills - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\ExportJob[] $exportJobs - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\ImportJob[] $importJobs - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\PiggyBank[] $piggyBanks - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Preference[] $preferences - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Role[] $roles - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\RuleGroup[] $ruleGroups - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Rule[] $rules - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Tag[] $tags - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournal[] $transactionJournals - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Transaction[] $transactions - * @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications - * @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $unreadNotifications - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\User whereId($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\User whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\User whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\User whereEmail($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\User wherePassword($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\User whereRememberToken($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\User whereReset($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\User whereBlocked($value) - * @method static \Illuminate\Database\Query\Builder|\FireflyIII\User whereBlockedCode($value) - * @mixin \Eloquent */ class User extends Authenticatable { @@ -121,6 +87,14 @@ class User extends Authenticatable return $this->hasMany('FireflyIII\Models\Attachment'); } + /** + * @return HasMany + */ + public function availableBudgets(): HasMany + { + return $this->hasMany('FireflyIII\Models\AvailableBudget'); + } + /** * @return HasMany */ @@ -222,6 +196,20 @@ class User extends Authenticatable return $this->hasMany('FireflyIII\Models\Rule'); } + /** + * Send the password reset notification. + * + * @param string $token + * + * @return void + */ + public function sendPasswordResetNotification($token) + { + $ip = Request::ip(); + + event(new RequestedNewPassword($this, $token, $ip)); + } + /** * @return HasMany */ diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index 8a272dd397..f38a164993 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -27,9 +27,9 @@ use FireflyIII\Rules\Triggers\TriggerInterface; use FireflyIII\User; use Google2FA; use Illuminate\Contracts\Encryption\DecryptException; +use Illuminate\Contracts\Translation\Translator; use Illuminate\Validation\Validator; use Session; -use Symfony\Component\Translation\TranslatorInterface; /** * Class FireflyValidator @@ -40,25 +40,24 @@ class FireflyValidator extends Validator { /** - * @param TranslatorInterface $translator - * @param array $data - * @param array $rules - * @param array $messages - * @param array $customAttributes + * @param Translator $translator + * @param array $data + * @param array $rules + * @param array $messages + * @param array $customAttributes * */ - public function __construct(TranslatorInterface $translator, array $data, array $rules, array $messages = [], array $customAttributes = []) + public function __construct(Translator $translator, array $data, array $rules, array $messages = [], array $customAttributes = []) { parent::__construct($translator, $data, $rules, $messages, $customAttributes); } /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param $attribute * @param $value * * @return bool - * @internal param $parameters - * * */ public function validate2faCode($attribute, $value): bool @@ -73,6 +72,7 @@ class FireflyValidator extends Validator } /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param $attribute * @param $value * @param $parameters @@ -96,6 +96,29 @@ class FireflyValidator extends Validator } /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param $attribute + * @param $value + * + * @return bool + * + */ + public function validateBic($attribute, $value): bool + { + $regex = '/^[a-z]{6}[0-9a-z]{2}([0-9a-z]{3})?\z/i'; + $result = preg_match($regex, $value); + if ($result === false) { + return false; + } + if ($result === 0) { + return false; + } + + return true; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param $attribute * @param $value * @@ -125,6 +148,22 @@ class FireflyValidator extends Validator } /** + * @param $attribute + * @param $value + * @param $parameters + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @return bool + */ + public function validateMore($attribute, $value, $parameters): bool + { + $compare = $parameters[0] ?? '0'; + + return bccomp($value, $compare) > 0; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param $attribute * @param $value * @param $parameters @@ -237,6 +276,7 @@ class FireflyValidator extends Validator } /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param $attribute * @param $value * @param $parameters @@ -267,6 +307,7 @@ class FireflyValidator extends Validator } /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param $attribute * @param $value * @param $parameters @@ -277,8 +318,7 @@ class FireflyValidator extends Validator { $accountId = $this->data['id'] ?? 0; - $query = AccountMeta:: - leftJoin('accounts', 'accounts.id', '=', 'account_meta.account_id') + $query = AccountMeta::leftJoin('accounts', 'accounts.id', '=', 'account_meta.account_id') ->where('accounts.user_id', auth()->user()->id) ->where('account_meta.name', 'accountNumber'); @@ -301,30 +341,7 @@ class FireflyValidator extends Validator } /** - * @param $attribute - * @param $value - * @param $parameters - * - * - * @return bool - */ - public function validateUniqueForUser($attribute, $value, $parameters): bool - { - $query = DB::table($parameters[0])->where($parameters[1], $value); - $query->where('user_id', auth()->user()->id); - if (isset($parameters[2])) { - $query->where('id', '!=', $parameters[2]); - } - $count = $query->count(); - if ($count == 0) { - return true; - } - - return false; - - } - - /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * Validate an object and its unicity. Checks for encryption / encrypted values as well. * * parameter 0: the table @@ -362,6 +379,7 @@ class FireflyValidator extends Validator } /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param $attribute * @param $value * @param $parameters @@ -455,7 +473,6 @@ class FireflyValidator extends Validator * @param $value * * @return bool - * @internal param $parameters * */ private function validateByAccountId($value): bool diff --git a/bootstrap/app.php b/bootstrap/app.php old mode 100755 new mode 100644 index dd875c174b..b026728e73 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -23,8 +23,7 @@ declare(strict_types = 1); | */ -bcscale(4); - +bcscale(12); $app = new Illuminate\Foundation\Application( realpath(__DIR__.'/../') diff --git a/bootstrap/autoload.php b/bootstrap/autoload.php old mode 100755 new mode 100644 diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore old mode 100755 new mode 100644 diff --git a/composer.json b/composer.json index b443593700..aa09eb9b44 100755 --- a/composer.json +++ b/composer.json @@ -5,49 +5,70 @@ "finance", "finances", "manager", + "management", "euro", + "dollar", "laravel", "money", + "currency", "financials", + "financial", "budgets", + "administration", + "tool", + "tooling", + "help", + "helper", + "assistant", + "planning", + "organizing", + "bills", + "personal finance", + "budgets", + "budgeting", + "budgeting tool", + "budgeting application", "transactions", + "self hosted", + "self-hosted", "transfers", "management" ], - "license": "MIT", - "homepage": "https://github.com/JC5/firefly-iii", + "license": "Creative Commons Attribution-ShareAlike 4.0 International License", + "homepage": "https://github.com/firefly-iii/firefly-iii", "type": "project", "authors": [ { "name": "James Cole", "email": "thegrumpydictator@gmail.com", - "homepage": "https://github.com/JC5", + "homepage": "https://github.com/firefly-iii", "role": "Developer" } ], "require": { "php": ">=7.0.0", "ext-intl": "*", - "laravel/framework": "5.3.18", - "davejamesmiller/laravel-breadcrumbs": "^3.0", - "watson/validating": "^3.0", + "laravel/framework": "5.4.*", + "davejamesmiller/laravel-breadcrumbs": "3.*", + "watson/validating": "3.*", "doctrine/dbal": "^2.5", - "league/commonmark": "^0.15.0", - "rcrowe/twigbridge": "^0.9.3", - "league/csv": "^8.1", - "laravelcollective/html": "^5.3", - "rmccue/requests": "^1.6", - "pragmarx/google2fa": "^1.0", - "barryvdh/laravel-debugbar": "^2.2", - "barryvdh/laravel-ide-helper": "^2.2", - "bacon/bacon-qr-code": "^1.0" + "league/commonmark": "0.15.*", + "twig/twig": "1.30.0", + "rcrowe/twigbridge": "0.9.*", + "league/csv": "8.*", + "laravelcollective/html": "^5.4", + "rmccue/requests": "1.*", + "pragmarx/google2fa": "1.*", + "bacon/bacon-qr-code": "1.*" }, "require-dev": { "fzaninotto/faker": "~1.4", "mockery/mockery": "0.9.*", "phpunit/phpunit": "~5.0", "symfony/css-selector": "3.1.*", - "symfony/dom-crawler": "3.1.*" + "symfony/dom-crawler": "3.1.*", + "barryvdh/laravel-debugbar": "2.*", + "barryvdh/laravel-ide-helper": "2.*" }, "autoload": { "classmap": [ @@ -58,9 +79,9 @@ } }, "autoload-dev": { - "classmap": [ - "tests/TestCase.php" - ] + "psr-4": { + "Tests\\": "tests/" + } }, "scripts": { "post-root-package-install": [ @@ -71,13 +92,14 @@ ], "post-install-cmd": [ "Illuminate\\Foundation\\ComposerScripts::postInstall", - "php artisan optimize" + "php artisan optimize", + "php artisan firefly:instructions install" ], "post-update-cmd": [ "Illuminate\\Foundation\\ComposerScripts::postUpdate", - "php artisan firefly:upgrade-instructions", "php artisan firefly:upgrade-database", "php artisan firefly:verify", + "php artisan firefly:instructions update", "php artisan optimize" ] }, diff --git a/composer.lock b/composer.lock index 0b571ce03d..d7fca941ad 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "cc3d23620e727ee1f4741b2e83f8685f", - "content-hash": "473d3c681e5c41989e9dced651a939df", + "content-hash": "254ffef07304b93679b48182a179123f", "packages": [ { "name": "bacon/bacon-qr-code", @@ -47,176 +46,7 @@ ], "description": "BaconQrCode is a QR code generator for PHP.", "homepage": "https://github.com/Bacon/BaconQrCode", - "time": "2016-01-09 22:55:35" - }, - { - "name": "barryvdh/laravel-debugbar", - "version": "v2.3.0", - "source": { - "type": "git", - "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "0c87981df959c7c1943abe227baf607c92f204f9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/0c87981df959c7c1943abe227baf607c92f204f9", - "reference": "0c87981df959c7c1943abe227baf607c92f204f9", - "shasum": "" - }, - "require": { - "illuminate/support": "5.1.*|5.2.*|5.3.*", - "maximebf/debugbar": "~1.13.0", - "php": ">=5.5.9", - "symfony/finder": "~2.7|~3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3-dev" - } - }, - "autoload": { - "psr-4": { - "Barryvdh\\Debugbar\\": "src/" - }, - "files": [ - "src/helpers.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" - } - ], - "description": "PHP Debugbar integration for Laravel", - "keywords": [ - "debug", - "debugbar", - "laravel", - "profiler", - "webprofiler" - ], - "time": "2016-09-15 14:05:56" - }, - { - "name": "barryvdh/laravel-ide-helper", - "version": "v2.2.1", - "source": { - "type": "git", - "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "28af7cd19ca41cc0c63dd1de2b46c2b84d31c463" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/28af7cd19ca41cc0c63dd1de2b46c2b84d31c463", - "reference": "28af7cd19ca41cc0c63dd1de2b46c2b84d31c463", - "shasum": "" - }, - "require": { - "barryvdh/reflection-docblock": "^2.0.4", - "illuminate/console": "^5.0,<5.4", - "illuminate/filesystem": "^5.0,<5.4", - "illuminate/support": "^5.0,<5.4", - "php": ">=5.4.0", - "symfony/class-loader": "^2.3|^3.0" - }, - "require-dev": { - "doctrine/dbal": "~2.3", - "phpunit/phpunit": "4.*", - "scrutinizer/ocular": "~1.1", - "squizlabs/php_codesniffer": "~2.3" - }, - "suggest": { - "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2-dev" - } - }, - "autoload": { - "psr-4": { - "Barryvdh\\LaravelIdeHelper\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" - } - ], - "description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.", - "keywords": [ - "autocomplete", - "codeintel", - "helper", - "ide", - "laravel", - "netbeans", - "phpdoc", - "phpstorm", - "sublime" - ], - "time": "2016-07-04 11:52:48" - }, - { - "name": "barryvdh/reflection-docblock", - "version": "v2.0.4", - "source": { - "type": "git", - "url": "https://github.com/barryvdh/ReflectionDocBlock.git", - "reference": "3dcbd98b5d9384a5357266efba8fd29884458e5c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/3dcbd98b5d9384a5357266efba8fd29884458e5c", - "reference": "3dcbd98b5d9384a5357266efba8fd29884458e5c", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.0,<4.5" - }, - "suggest": { - "dflydev/markdown": "~1.0", - "erusev/parsedown": "~1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "Barryvdh": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" - } - ], - "time": "2016-06-13 19:28:20" + "time": "2016-01-09T22:55:35+00:00" }, { "name": "christian-riesen/base32", @@ -270,74 +100,20 @@ "encode", "rfc4648" ], - "time": "2016-05-05 11:49:03" - }, - { - "name": "classpreloader/classpreloader", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/ClassPreloader/ClassPreloader.git", - "reference": "9b10b913c2bdf90c3d2e0d726b454fb7f77c552a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ClassPreloader/ClassPreloader/zipball/9b10b913c2bdf90c3d2e0d726b454fb7f77c552a", - "reference": "9b10b913c2bdf90c3d2e0d726b454fb7f77c552a", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^1.0|^2.0", - "php": ">=5.5.9" - }, - "require-dev": { - "phpunit/phpunit": "^4.8|^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "ClassPreloader\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com" - }, - { - "name": "Graham Campbell", - "email": "graham@alt-three.com" - } - ], - "description": "Helps class loading performance by generating a single PHP file containing all of the autoloaded files for a specific use case", - "keywords": [ - "autoload", - "class", - "preload" - ], - "time": "2015-11-09 22:51:51" + "time": "2016-05-05T11:49:03+00:00" }, { "name": "davejamesmiller/laravel-breadcrumbs", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/davejamesmiller/laravel-breadcrumbs.git", - "reference": "460bf79e83ff9e3db1e3f1c40169d8893893f8ff" + "reference": "6ca5a600003ecb52a5b5af14dad82033058604e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/davejamesmiller/laravel-breadcrumbs/zipball/460bf79e83ff9e3db1e3f1c40169d8893893f8ff", - "reference": "460bf79e83ff9e3db1e3f1c40169d8893893f8ff", + "url": "https://api.github.com/repos/davejamesmiller/laravel-breadcrumbs/zipball/6ca5a600003ecb52a5b5af14dad82033058604e1", + "reference": "6ca5a600003ecb52a5b5af14dad82033058604e1", "shasum": "" }, "require": { @@ -365,80 +141,47 @@ { "name": "Dave James Miller", "email": "dave@davejamesmiller.com", - "homepage": "http://davejamesmiller.com/" + "homepage": "https://davejamesmiller.com/" } ], "description": "A simple Laravel-style way to create breadcrumbs in Laravel 4+.", - "homepage": "http://laravel-breadcrumbs.davejamesmiller.com", + "homepage": "https://laravel-breadcrumbs.readthedocs.io/", "keywords": [ "laravel" ], - "time": "2016-08-28 16:57:03" - }, - { - "name": "dnoegel/php-xdg-base-dir", - "version": "0.1", - "source": { - "type": "git", - "url": "https://github.com/dnoegel/php-xdg-base-dir.git", - "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/265b8593498b997dc2d31e75b89f053b5cc9621a", - "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "require-dev": { - "phpunit/phpunit": "@stable" - }, - "type": "project", - "autoload": { - "psr-4": { - "XdgBaseDir\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "implementation of xdg base directory specification for php", - "time": "2014-10-24 07:27:01" + "time": "2017-01-30T21:16:53+00:00" }, { "name": "doctrine/annotations", - "version": "v1.2.7", + "version": "v1.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535" + "reference": "bd4461328621bde0ae6b1b2675fbc6aca4ceb558" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/f25c8aab83e0c3e976fd7d19875f198ccf2f7535", - "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/bd4461328621bde0ae6b1b2675fbc6aca4ceb558", + "reference": "bd4461328621bde0ae6b1b2675fbc6aca4ceb558", "shasum": "" }, "require": { "doctrine/lexer": "1.*", - "php": ">=5.3.2" + "php": "^5.6 || ^7.0" }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "4.*" + "phpunit/phpunit": "^5.6.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "1.4.x-dev" } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Annotations\\": "lib/" + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" } }, "notification-url": "https://packagist.org/downloads/", @@ -474,20 +217,20 @@ "docblock", "parser" ], - "time": "2015-08-31 12:32:49" + "time": "2016-12-30T15:59:45+00:00" }, { "name": "doctrine/cache", - "version": "v1.6.0", + "version": "v1.6.1", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6" + "reference": "b6f544a20f4807e81f7044d31e679ccbb1866dc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/f8af318d14bdb0eff0336795b428b547bd39ccb6", - "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6", + "url": "https://api.github.com/repos/doctrine/cache/zipball/b6f544a20f4807e81f7044d31e679ccbb1866dc3", + "reference": "b6f544a20f4807e81f7044d31e679ccbb1866dc3", "shasum": "" }, "require": { @@ -544,32 +287,33 @@ "cache", "caching" ], - "time": "2015-12-31 16:37:02" + "time": "2016-10-29T11:16:17+00:00" }, { "name": "doctrine/collections", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a" + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/6c1e4eef75f310ea1b3e30945e9f06e652128b8a", - "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "doctrine/coding-standard": "~0.1@dev", + "phpunit/phpunit": "^5.7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { @@ -610,20 +354,20 @@ "collections", "iterator" ], - "time": "2015-04-14 22:21:58" + "time": "2017-01-03T10:49:41+00:00" }, { "name": "doctrine/common", - "version": "v2.6.1", + "version": "v2.7.2", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "a579557bc689580c19fee4e27487a67fe60defc0" + "reference": "930297026c8009a567ac051fd545bf6124150347" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/a579557bc689580c19fee4e27487a67fe60defc0", - "reference": "a579557bc689580c19fee4e27487a67fe60defc0", + "url": "https://api.github.com/repos/doctrine/common/zipball/930297026c8009a567ac051fd545bf6124150347", + "reference": "930297026c8009a567ac051fd545bf6124150347", "shasum": "" }, "require": { @@ -632,10 +376,10 @@ "doctrine/collections": "1.*", "doctrine/inflector": "1.*", "doctrine/lexer": "1.*", - "php": "~5.5|~7.0" + "php": "~5.6|~7.0" }, "require-dev": { - "phpunit/phpunit": "~4.8|~5.0" + "phpunit/phpunit": "^5.4.6" }, "type": "library", "extra": { @@ -683,24 +427,24 @@ "persistence", "spl" ], - "time": "2015-12-25 13:18:31" + "time": "2017-01-13T14:02:13+00:00" }, { "name": "doctrine/dbal", - "version": "v2.5.5", + "version": "v2.5.12", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "9f8c05cd5225a320d56d4bfdb4772f10d045a0c9" + "reference": "7b9e911f9d8b30d43b96853dab26898c710d8f44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/9f8c05cd5225a320d56d4bfdb4772f10d045a0c9", - "reference": "9f8c05cd5225a320d56d4bfdb4772f10d045a0c9", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/7b9e911f9d8b30d43b96853dab26898c710d8f44", + "reference": "7b9e911f9d8b30d43b96853dab26898c710d8f44", "shasum": "" }, "require": { - "doctrine/common": ">=2.4,<2.7-dev", + "doctrine/common": ">=2.4,<2.8-dev", "php": ">=5.3.2" }, "require-dev": { @@ -754,7 +498,7 @@ "persistence", "queryobject" ], - "time": "2016-09-09 19:13:33" + "time": "2017-02-08T12:53:47+00:00" }, { "name": "doctrine/inflector", @@ -821,7 +565,7 @@ "singularize", "string" ], - "time": "2015-11-06 14:35:42" + "time": "2015-11-06T14:35:42+00:00" }, { "name": "doctrine/lexer", @@ -875,80 +619,29 @@ "lexer", "parser" ], - "time": "2014-09-09 13:34:57" + "time": "2014-09-09T13:34:57+00:00" }, { - "name": "jakub-onderka/php-console-color", - "version": "0.1", + "name": "erusev/parsedown", + "version": "1.6.1", "source": { "type": "git", - "url": "https://github.com/JakubOnderka/PHP-Console-Color.git", - "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1" + "url": "https://github.com/erusev/parsedown.git", + "reference": "20ff8bbb57205368b4b42d094642a3e52dac85fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/e0b393dacf7703fc36a4efc3df1435485197e6c1", - "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/20ff8bbb57205368b4b42d094642a3e52dac85fb", + "reference": "20ff8bbb57205368b4b42d094642a3e52dac85fb", "shasum": "" }, "require": { - "php": ">=5.3.2" - }, - "require-dev": { - "jakub-onderka/php-code-style": "1.0", - "jakub-onderka/php-parallel-lint": "0.*", - "jakub-onderka/php-var-dump-check": "0.*", - "phpunit/phpunit": "3.7.*", - "squizlabs/php_codesniffer": "1.*" - }, - "type": "library", - "autoload": { - "psr-0": { - "JakubOnderka\\PhpConsoleColor": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-2-Clause" - ], - "authors": [ - { - "name": "Jakub Onderka", - "email": "jakub.onderka@gmail.com", - "homepage": "http://www.acci.cz" - } - ], - "time": "2014-04-08 15:00:19" - }, - { - "name": "jakub-onderka/php-console-highlighter", - "version": "v0.3.2", - "source": { - "type": "git", - "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git", - "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/7daa75df45242c8d5b75a22c00a201e7954e4fb5", - "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5", - "shasum": "" - }, - "require": { - "jakub-onderka/php-console-color": "~0.1", "php": ">=5.3.0" }, - "require-dev": { - "jakub-onderka/php-code-style": "~1.0", - "jakub-onderka/php-parallel-lint": "~0.5", - "jakub-onderka/php-var-dump-check": "~0.1", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~1.5" - }, "type": "library", "autoload": { "psr-0": { - "JakubOnderka\\PhpConsoleHighlighter": "src/" + "Parsedown": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -957,109 +650,55 @@ ], "authors": [ { - "name": "Jakub Onderka", - "email": "acci@acci.cz", - "homepage": "http://www.acci.cz/" + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" } ], - "time": "2015-04-20 18:58:01" - }, - { - "name": "jeremeamia/SuperClosure", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/jeremeamia/super_closure.git", - "reference": "29a88be2a4846d27c1613aed0c9071dfad7b5938" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jeremeamia/super_closure/zipball/29a88be2a4846d27c1613aed0c9071dfad7b5938", - "reference": "29a88be2a4846d27c1613aed0c9071dfad7b5938", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^1.2|^2.0", - "php": ">=5.4", - "symfony/polyfill-php56": "^1.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0|^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2-dev" - } - }, - "autoload": { - "psr-4": { - "SuperClosure\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jeremy Lindblom", - "email": "jeremeamia@gmail.com", - "homepage": "https://github.com/jeremeamia", - "role": "Developer" - } - ], - "description": "Serialize Closure objects, including their context and binding", - "homepage": "https://github.com/jeremeamia/super_closure", + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", "keywords": [ - "closure", - "function", - "lambda", - "parser", - "serializable", - "serialize", - "tokenizer" + "markdown", + "parser" ], - "time": "2015-12-05 17:17:57" + "time": "2016-11-02T15:56:58+00:00" }, { "name": "laravel/framework", - "version": "v5.3.18", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "9bee167d173857c25966c19afdaa66f127ca6784" + "reference": "707f32d32dce58232f7a860e0a1d62caf6f9dbfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/9bee167d173857c25966c19afdaa66f127ca6784", - "reference": "9bee167d173857c25966c19afdaa66f127ca6784", + "url": "https://api.github.com/repos/laravel/framework/zipball/707f32d32dce58232f7a860e0a1d62caf6f9dbfc", + "reference": "707f32d32dce58232f7a860e0a1d62caf6f9dbfc", "shasum": "" }, "require": { - "classpreloader/classpreloader": "~3.0", "doctrine/inflector": "~1.0", + "erusev/parsedown": "~1.6", "ext-mbstring": "*", "ext-openssl": "*", - "jeremeamia/superclosure": "~2.2", "league/flysystem": "~1.0", "monolog/monolog": "~1.11", "mtdowling/cron-expression": "~1.0", "nesbot/carbon": "~1.20", "paragonie/random_compat": "~1.4|~2.0", "php": ">=5.6.4", - "psy/psysh": "0.7.*", "ramsey/uuid": "~3.0", - "swiftmailer/swiftmailer": "~5.1", - "symfony/console": "3.1.*", - "symfony/debug": "3.1.*", - "symfony/finder": "3.1.*", - "symfony/http-foundation": "3.1.*", - "symfony/http-kernel": "3.1.*", - "symfony/process": "3.1.*", - "symfony/routing": "3.1.*", - "symfony/translation": "3.1.*", - "symfony/var-dumper": "3.1.*", + "swiftmailer/swiftmailer": "~5.4", + "symfony/console": "~3.2", + "symfony/debug": "~3.2", + "symfony/finder": "~3.2", + "symfony/http-foundation": "~3.2", + "symfony/http-kernel": "~3.2", + "symfony/process": "~3.2", + "symfony/routing": "~3.2", + "symfony/var-dumper": "~3.2", + "tijsverkoyen/css-to-inline-styles": "~2.2", "vlucas/phpdotenv": "~2.2" }, "replace": { @@ -1096,31 +735,34 @@ }, "require-dev": { "aws/aws-sdk-php": "~3.0", + "doctrine/dbal": "~2.5", "mockery/mockery": "~0.9.4", "pda/pheanstalk": "~3.0", - "phpunit/phpunit": "~5.4", + "phpunit/phpunit": "~5.7", "predis/predis": "~1.0", - "symfony/css-selector": "3.1.*", - "symfony/dom-crawler": "3.1.*" + "symfony/css-selector": "~3.2", + "symfony/dom-crawler": "~3.2" }, "suggest": { "aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (~3.0).", - "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.4).", + "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.5).", "fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).", - "guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~5.3|~6.0).", + "guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).", + "laravel/tinker": "Required to use the tinker console command (~1.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).", "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).", + "nexmo/client": "Required to use the Nexmo transport (~1.0).", "pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).", "predis/predis": "Required to use the redis cache and queue drivers (~1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~2.0).", - "symfony/css-selector": "Required to use some of the crawler integration testing tools (3.1.*).", - "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (3.1.*).", + "symfony/css-selector": "Required to use some of the crawler integration testing tools (~3.2).", + "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (~3.2).", "symfony/psr-http-message-bridge": "Required to psr7 bridging features (0.2.*)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.3-dev" + "dev-master": "5.4-dev" } }, "autoload": { @@ -1148,32 +790,32 @@ "framework", "laravel" ], - "time": "2016-10-08 01:51:20" + "time": "2017-02-15T14:31:32+00:00" }, { "name": "laravelcollective/html", - "version": "v5.3.0", + "version": "v5.4.1", "source": { "type": "git", "url": "https://github.com/LaravelCollective/html.git", - "reference": "961ce141c16c6b085128f209496c26efd3e681ca" + "reference": "7570f25d58a00fd6909c0563808590f9cdb14d47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/LaravelCollective/html/zipball/961ce141c16c6b085128f209496c26efd3e681ca", - "reference": "961ce141c16c6b085128f209496c26efd3e681ca", + "url": "https://api.github.com/repos/LaravelCollective/html/zipball/7570f25d58a00fd6909c0563808590f9cdb14d47", + "reference": "7570f25d58a00fd6909c0563808590f9cdb14d47", "shasum": "" }, "require": { - "illuminate/http": "5.3.*", - "illuminate/routing": "5.3.*", - "illuminate/session": "5.3.*", - "illuminate/support": "5.3.*", - "illuminate/view": "5.3.*", + "illuminate/http": "5.4.*", + "illuminate/routing": "5.4.*", + "illuminate/session": "5.4.*", + "illuminate/support": "5.4.*", + "illuminate/view": "5.4.*", "php": ">=5.6.4" }, "require-dev": { - "illuminate/database": "5.3.*", + "illuminate/database": "5.4.*", "mockery/mockery": "~0.9.4", "phpunit/phpunit": "~5.4" }, @@ -1202,20 +844,20 @@ ], "description": "HTML and Form Builders for the Laravel Framework", "homepage": "http://laravelcollective.com", - "time": "2016-08-27 23:52:43" + "time": "2017-01-26T19:27:05+00:00" }, { "name": "league/commonmark", - "version": "0.15.0", + "version": "0.15.3", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "19fb96643beba24e681c371dc133e25409742664" + "reference": "c8b43ee5821362216f8e9ac684f0f59de164edcc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/19fb96643beba24e681c371dc133e25409742664", - "reference": "19fb96643beba24e681c371dc133e25409742664", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c8b43ee5821362216f8e9ac684f0f59de164edcc", + "reference": "c8b43ee5821362216f8e9ac684f0f59de164edcc", "shasum": "" }, "require": { @@ -1228,7 +870,7 @@ "require-dev": { "cebe/markdown": "~1.0", "erusev/parsedown": "~1.0", - "jgm/commonmark": "0.26", + "jgm/commonmark": "0.27", "michelf/php-markdown": "~1.4", "mikehaertl/php-shellcommand": "~1.2.0", "phpunit/phpunit": "~4.3|~5.0", @@ -1271,20 +913,20 @@ "markdown", "parser" ], - "time": "2016-09-14 15:44:35" + "time": "2016-12-19T00:11:43+00:00" }, { "name": "league/csv", - "version": "8.1.1", + "version": "8.2.0", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "3b22a40804aa0bc5224ffb2f5e8248edf0a9a38c" + "reference": "ef7eef710810c8bd0cf9371582ccd0123ff96d4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/3b22a40804aa0bc5224ffb2f5e8248edf0a9a38c", - "reference": "3b22a40804aa0bc5224ffb2f5e8248edf0a9a38c", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/ef7eef710810c8bd0cf9371582ccd0123ff96d4b", + "reference": "ef7eef710810c8bd0cf9371582ccd0123ff96d4b", "shasum": "" }, "require": { @@ -1298,7 +940,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "8.0-dev" + "dev-master": "8.2-dev" } }, "autoload": { @@ -1328,20 +970,20 @@ "read", "write" ], - "time": "2016-09-05 08:16:07" + "time": "2017-01-25T13:32:07+00:00" }, { "name": "league/flysystem", - "version": "1.0.32", + "version": "1.0.35", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "1b5c4a0031697f46e779a9d1b309c2e1b24daeab" + "reference": "dda7f3ab94158a002d9846a97dc18ebfb7acc062" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1b5c4a0031697f46e779a9d1b309c2e1b24daeab", - "reference": "1b5c4a0031697f46e779a9d1b309c2e1b24daeab", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/dda7f3ab94158a002d9846a97dc18ebfb7acc062", + "reference": "dda7f3ab94158a002d9846a97dc18ebfb7acc062", "shasum": "" }, "require": { @@ -1411,81 +1053,20 @@ "sftp", "storage" ], - "time": "2016-10-19 20:38:46" - }, - { - "name": "maximebf/debugbar", - "version": "v1.13.0", - "source": { - "type": "git", - "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "5f49a5ed6cfde81d31d89378806670d77462526e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/5f49a5ed6cfde81d31d89378806670d77462526e", - "reference": "5f49a5ed6cfde81d31d89378806670d77462526e", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "psr/log": "^1.0", - "symfony/var-dumper": "^2.6|^3.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0|^5.0" - }, - "suggest": { - "kriswallsmith/assetic": "The best way to manage assets", - "monolog/monolog": "Log using Monolog", - "predis/predis": "Redis storage" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.13-dev" - } - }, - "autoload": { - "psr-4": { - "DebugBar\\": "src/DebugBar/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Maxime Bouroumeau-Fuseau", - "email": "maxime.bouroumeau@gmail.com", - "homepage": "http://maximebf.com" - }, - { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" - } - ], - "description": "Debug bar in the browser for php application", - "homepage": "https://github.com/maximebf/php-debugbar", - "keywords": [ - "debug", - "debugbar" - ], - "time": "2016-09-15 14:01:59" + "time": "2017-02-09T11:33:58+00:00" }, { "name": "monolog/monolog", - "version": "1.21.0", + "version": "1.22.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "f42fbdfd53e306bda545845e4dbfd3e72edb4952" + "reference": "bad29cb8d18ab0315e6c477751418a82c850d558" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f42fbdfd53e306bda545845e4dbfd3e72edb4952", - "reference": "f42fbdfd53e306bda545845e4dbfd3e72edb4952", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bad29cb8d18ab0315e6c477751418a82c850d558", + "reference": "bad29cb8d18ab0315e6c477751418a82c850d558", "shasum": "" }, "require": { @@ -1496,7 +1077,7 @@ "psr/log-implementation": "1.0.0" }, "require-dev": { - "aws/aws-sdk-php": "^2.4.9", + "aws/aws-sdk-php": "^2.4.9 || ^3.0", "doctrine/couchdb": "~1.0@dev", "graylog2/gelf-php": "~1.0", "jakub-onderka/php-parallel-lint": "0.9", @@ -1550,20 +1131,20 @@ "logging", "psr-3" ], - "time": "2016-07-29 03:23:52" + "time": "2016-11-26T00:15:39+00:00" }, { "name": "mtdowling/cron-expression", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/mtdowling/cron-expression.git", - "reference": "c9ee7886f5a12902b225a1a12f36bb45f9ab89e5" + "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/c9ee7886f5a12902b225a1a12f36bb45f9ab89e5", - "reference": "c9ee7886f5a12902b225a1a12f36bb45f9ab89e5", + "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad", + "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad", "shasum": "" }, "require": { @@ -1574,8 +1155,8 @@ }, "type": "library", "autoload": { - "psr-0": { - "Cron": "src/" + "psr-4": { + "Cron\\": "src/Cron/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1594,30 +1175,36 @@ "cron", "schedule" ], - "time": "2016-01-26 21:23:30" + "time": "2017-01-23T04:29:33+00:00" }, { "name": "nesbot/carbon", - "version": "1.21.0", + "version": "1.22.1", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "7b08ec6f75791e130012f206e3f7b0e76e18e3d7" + "reference": "7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/7b08ec6f75791e130012f206e3f7b0e76e18e3d7", - "reference": "7b08ec6f75791e130012f206e3f7b0e76e18e3d7", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc", + "reference": "7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc", "shasum": "" }, "require": { "php": ">=5.3.0", - "symfony/translation": "~2.6|~3.0" + "symfony/translation": "~2.6 || ~3.0" }, "require-dev": { - "phpunit/phpunit": "~4.0|~5.0" + "friendsofphp/php-cs-fixer": "~2", + "phpunit/phpunit": "~4.0 || ~5.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.23-dev" + } + }, "autoload": { "psr-4": { "Carbon\\": "src/Carbon/" @@ -1641,71 +1228,20 @@ "datetime", "time" ], - "time": "2015-11-04 20:07:17" - }, - { - "name": "nikic/php-parser", - "version": "v2.1.1", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4dd659edadffdc2143e4753df655d866dbfeedf0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4dd659edadffdc2143e4753df655d866dbfeedf0", - "reference": "4dd659edadffdc2143e4753df655d866dbfeedf0", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=5.4" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "time": "2016-09-16 12:04:44" + "time": "2017-01-16T07:55:07+00:00" }, { "name": "paragonie/random_compat", - "version": "v2.0.3", + "version": "v2.0.4", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "c0125896dbb151380ab47e96c621741e79623beb" + "reference": "a9b97968bcde1c4de2a5ec6cbd06a0f6c919b46e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/c0125896dbb151380ab47e96c621741e79623beb", - "reference": "c0125896dbb151380ab47e96c621741e79623beb", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/a9b97968bcde1c4de2a5ec6cbd06a0f6c919b46e", + "reference": "a9b97968bcde1c4de2a5ec6cbd06a0f6c919b46e", "shasum": "" }, "require": { @@ -1740,7 +1276,7 @@ "pseudorandom", "random" ], - "time": "2016-10-17 15:23:22" + "time": "2016-11-07T23:38:38+00:00" }, { "name": "pragmarx/google2fa", @@ -1801,7 +1337,7 @@ "google2fa", "laravel" ], - "time": "2016-07-18 20:25:04" + "time": "2016-07-18T20:25:04+00:00" }, { "name": "psr/log", @@ -1848,92 +1384,20 @@ "psr", "psr-3" ], - "time": "2016-10-10 12:19:37" - }, - { - "name": "psy/psysh", - "version": "v0.7.2", - "source": { - "type": "git", - "url": "https://github.com/bobthecow/psysh.git", - "reference": "e64e10b20f8d229cac76399e1f3edddb57a0f280" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/e64e10b20f8d229cac76399e1f3edddb57a0f280", - "reference": "e64e10b20f8d229cac76399e1f3edddb57a0f280", - "shasum": "" - }, - "require": { - "dnoegel/php-xdg-base-dir": "0.1", - "jakub-onderka/php-console-highlighter": "0.3.*", - "nikic/php-parser": "^1.2.1|~2.0", - "php": ">=5.3.9", - "symfony/console": "~2.3.10|^2.4.2|~3.0", - "symfony/var-dumper": "~2.7|~3.0" - }, - "require-dev": { - "fabpot/php-cs-fixer": "~1.5", - "phpunit/phpunit": "~3.7|~4.0|~5.0", - "squizlabs/php_codesniffer": "~2.0", - "symfony/finder": "~2.1|~3.0" - }, - "suggest": { - "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", - "ext-pdo-sqlite": "The doc command requires SQLite to work.", - "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.", - "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history." - }, - "bin": [ - "bin/psysh" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-develop": "0.8.x-dev" - } - }, - "autoload": { - "files": [ - "src/Psy/functions.php" - ], - "psr-4": { - "Psy\\": "src/Psy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" - } - ], - "description": "An interactive shell for modern PHP.", - "homepage": "http://psysh.org", - "keywords": [ - "REPL", - "console", - "interactive", - "shell" - ], - "time": "2016-03-09 05:03:14" + "time": "2016-10-10T12:19:37+00:00" }, { "name": "ramsey/uuid", - "version": "3.5.1", + "version": "3.5.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "a07797b986671b0dc823885a81d5e3516b931599" + "reference": "5677cfe02397dd6b58c861870dfaa5d9007d3954" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/a07797b986671b0dc823885a81d5e3516b931599", - "reference": "a07797b986671b0dc823885a81d5e3516b931599", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/5677cfe02397dd6b58c861870dfaa5d9007d3954", + "reference": "5677cfe02397dd6b58c861870dfaa5d9007d3954", "shasum": "" }, "require": { @@ -1946,11 +1410,13 @@ "require-dev": { "apigen/apigen": "^4.1", "codeception/aspect-mock": "1.0.0", + "doctrine/annotations": "~1.2.0", "goaop/framework": "1.0.0-alpha.2", "ircmaxell/random-lib": "^1.1", "jakub-onderka/php-parallel-lint": "^0.9.0", "mockery/mockery": "^0.9.4", "moontoast/math": "^1.1", + "php-mock/php-mock-phpunit": "^0.3|^1.1", "phpunit/phpunit": "^4.7|>=5.0 <5.4", "satooshi/php-coveralls": "^0.6.1", "squizlabs/php_codesniffer": "^2.3" @@ -2000,27 +1466,27 @@ "identifier", "uuid" ], - "time": "2016-10-02 15:51:17" + "time": "2016-11-22T19:21:44+00:00" }, { "name": "rcrowe/twigbridge", - "version": "v0.9.3", + "version": "v0.9.4", "source": { "type": "git", "url": "https://github.com/rcrowe/TwigBridge.git", - "reference": "6226d33331bbb1cdf64593a786f7efd1670200a7" + "reference": "effda159c436b08eae1a9d9ba3d28bee8f7b0f3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/6226d33331bbb1cdf64593a786f7efd1670200a7", - "reference": "6226d33331bbb1cdf64593a786f7efd1670200a7", + "url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/effda159c436b08eae1a9d9ba3d28bee8f7b0f3f", + "reference": "effda159c436b08eae1a9d9ba3d28bee8f7b0f3f", "shasum": "" }, "require": { - "illuminate/support": "5.0.*|5.1.*|5.2.*|5.3.*", - "illuminate/view": "5.0.*|5.1.*|5.2.*|5.3.*", + "illuminate/support": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*", + "illuminate/view": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*", "php": ">=5.4.0", - "twig/twig": "~1.15|~2.0" + "twig/twig": "~1.30" }, "require-dev": { "laravel/framework": "5.0.*", @@ -2064,7 +1530,7 @@ "laravel", "twig" ], - "time": "2016-05-01 16:43:38" + "time": "2017-01-21T14:33:47+00:00" }, { "name": "rmccue/requests", @@ -2113,27 +1579,28 @@ "iri", "sockets" ], - "time": "2016-10-13 00:11:37" + "time": "2016-10-13T00:11:37+00:00" }, { "name": "swiftmailer/swiftmailer", - "version": "v5.4.3", + "version": "v5.4.6", "source": { "type": "git", "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "4cc92842069c2bbc1f28daaaf1d2576ec4dfe153" + "reference": "81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/4cc92842069c2bbc1f28daaaf1d2576ec4dfe153", - "reference": "4cc92842069c2bbc1f28daaaf1d2576ec4dfe153", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e", + "reference": "81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "mockery/mockery": "~0.9.1" + "mockery/mockery": "~0.9.1", + "symfony/phpunit-bridge": "~3.2" }, "type": "library", "extra": { @@ -2166,76 +1633,20 @@ "mail", "mailer" ], - "time": "2016-07-08 11:51:25" - }, - { - "name": "symfony/class-loader", - "version": "v3.1.5", - "source": { - "type": "git", - "url": "https://github.com/symfony/class-loader.git", - "reference": "bcb072aba46ddf3b1a496438c63be6be647739aa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/bcb072aba46ddf3b1a496438c63be6be647739aa", - "reference": "bcb072aba46ddf3b1a496438c63be6be647739aa", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "require-dev": { - "symfony/finder": "~2.8|~3.0", - "symfony/polyfill-apcu": "~1.1" - }, - "suggest": { - "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\ClassLoader\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony ClassLoader Component", - "homepage": "https://symfony.com", - "time": "2016-09-06 23:30:54" + "time": "2017-02-13T07:52:53+00:00" }, { "name": "symfony/console", - "version": "v3.1.5", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6cb0872fb57b38b3b09ff213c21ed693956b9eb0" + "reference": "7a8405a9fc175f87fed8a3c40856b0d866d61936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6cb0872fb57b38b3b09ff213c21ed693956b9eb0", - "reference": "6cb0872fb57b38b3b09ff213c21ed693956b9eb0", + "url": "https://api.github.com/repos/symfony/console/zipball/7a8405a9fc175f87fed8a3c40856b0d866d61936", + "reference": "7a8405a9fc175f87fed8a3c40856b0d866d61936", "shasum": "" }, "require": { @@ -2246,17 +1657,19 @@ "require-dev": { "psr/log": "~1.0", "symfony/event-dispatcher": "~2.8|~3.0", + "symfony/filesystem": "~2.8|~3.0", "symfony/process": "~2.8|~3.0" }, "suggest": { "psr/log": "For using the console logger", "symfony/event-dispatcher": "", + "symfony/filesystem": "", "symfony/process": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2283,20 +1696,73 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-09-28 00:11:12" + "time": "2017-02-06T12:04:21+00:00" }, { - "name": "symfony/debug", - "version": "v3.1.5", + "name": "symfony/css-selector", + "version": "v3.1.10", "source": { "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "e2b3f74a67fc928adc3c1b9027f73e1bc01190a8" + "url": "https://github.com/symfony/css-selector.git", + "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/e2b3f74a67fc928adc3c1b9027f73e1bc01190a8", - "reference": "e2b3f74a67fc928adc3c1b9027f73e1bc01190a8", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d", + "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2017-01-02T20:31:54+00:00" + }, + { + "name": "symfony/debug", + "version": "v3.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "b4d9818f127c60ce21ed62c395da7df868dc8477" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/b4d9818f127c60ce21ed62c395da7df868dc8477", + "reference": "b4d9818f127c60ce21ed62c395da7df868dc8477", "shasum": "" }, "require": { @@ -2313,7 +1779,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2340,20 +1806,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2016-09-06 11:02:40" + "time": "2017-01-28T02:37:08+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.1.5", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5" + "reference": "9137eb3a3328e413212826d63eeeb0217836e2b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c0c00c80b3a69132c4e55c3e7db32b4a387615e5", - "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9137eb3a3328e413212826d63eeeb0217836e2b6", + "reference": "9137eb3a3328e413212826d63eeeb0217836e2b6", "shasum": "" }, "require": { @@ -2373,7 +1839,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2400,20 +1866,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-07-19 10:45:57" + "time": "2017-01-02T20:32:22+00:00" }, { "name": "symfony/finder", - "version": "v3.1.5", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "205b5ffbb518a98ba2ae60a52656c4a31ab00c6f" + "reference": "8c71141cae8e2957946b403cc71a67213c0380d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/205b5ffbb518a98ba2ae60a52656c4a31ab00c6f", - "reference": "205b5ffbb518a98ba2ae60a52656c4a31ab00c6f", + "url": "https://api.github.com/repos/symfony/finder/zipball/8c71141cae8e2957946b403cc71a67213c0380d6", + "reference": "8c71141cae8e2957946b403cc71a67213c0380d6", "shasum": "" }, "require": { @@ -2422,7 +1888,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2449,20 +1915,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2016-09-28 00:11:12" + "time": "2017-01-02T20:32:22+00:00" }, { "name": "symfony/http-foundation", - "version": "v3.1.5", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "5114f1becca9f29e3af94374f1689c83c1aa3d97" + "reference": "e192b04de44aa1ed0e39d6793f7e06f5e0b672a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5114f1becca9f29e3af94374f1689c83c1aa3d97", - "reference": "5114f1becca9f29e3af94374f1689c83c1aa3d97", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e192b04de44aa1ed0e39d6793f7e06f5e0b672a0", + "reference": "e192b04de44aa1ed0e39d6793f7e06f5e0b672a0", "shasum": "" }, "require": { @@ -2475,7 +1941,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2502,20 +1968,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2016-09-21 20:55:10" + "time": "2017-02-02T13:47:35+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.1.5", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "dc339d6eebadfa6e39c52868b4d4a715eff13c69" + "reference": "96443239baf674b143604fb87cb27cb01672ab77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/dc339d6eebadfa6e39c52868b4d4a715eff13c69", - "reference": "dc339d6eebadfa6e39c52868b4d4a715eff13c69", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/96443239baf674b143604fb87cb27cb01672ab77", + "reference": "96443239baf674b143604fb87cb27cb01672ab77", "shasum": "" }, "require": { @@ -2523,7 +1989,7 @@ "psr/log": "~1.0", "symfony/debug": "~2.8|~3.0", "symfony/event-dispatcher": "~2.8|~3.0", - "symfony/http-foundation": "~2.8.8|~3.0.8|~3.1.2|~3.2" + "symfony/http-foundation": "~2.8.13|~3.1.6|~3.2" }, "conflict": { "symfony/config": "<2.8" @@ -2543,7 +2009,7 @@ "symfony/stopwatch": "~2.8|~3.0", "symfony/templating": "~2.8|~3.0", "symfony/translation": "~2.8|~3.0", - "symfony/var-dumper": "~2.8|~3.0" + "symfony/var-dumper": "~3.2" }, "suggest": { "symfony/browser-kit": "", @@ -2557,7 +2023,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2584,20 +2050,20 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2016-10-03 19:01:06" + "time": "2017-02-06T13:15:19+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "dff51f72b0706335131b00a7f49606168c582594" + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/dff51f72b0706335131b00a7f49606168c582594", - "reference": "dff51f72b0706335131b00a7f49606168c582594", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", "shasum": "" }, "require": { @@ -2609,7 +2075,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -2643,20 +2109,20 @@ "portable", "shim" ], - "time": "2016-05-18 14:26:46" + "time": "2016-11-14T01:06:16+00:00" }, { "name": "symfony/polyfill-php56", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php56.git", - "reference": "3edf57a8fbf9a927533344cef65ad7e1cf31030a" + "reference": "1dd42b9b89556f18092f3d1ada22cb05ac85383c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/3edf57a8fbf9a927533344cef65ad7e1cf31030a", - "reference": "3edf57a8fbf9a927533344cef65ad7e1cf31030a", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/1dd42b9b89556f18092f3d1ada22cb05ac85383c", + "reference": "1dd42b9b89556f18092f3d1ada22cb05ac85383c", "shasum": "" }, "require": { @@ -2666,7 +2132,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -2699,20 +2165,20 @@ "portable", "shim" ], - "time": "2016-05-18 14:26:46" + "time": "2016-11-14T01:06:16+00:00" }, { "name": "symfony/polyfill-util", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-util.git", - "reference": "ef830ce3d218e622b221d6bfad42c751d974bf99" + "reference": "746bce0fca664ac0a575e465f65c6643faddf7fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/ef830ce3d218e622b221d6bfad42c751d974bf99", - "reference": "ef830ce3d218e622b221d6bfad42c751d974bf99", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/746bce0fca664ac0a575e465f65c6643faddf7fb", + "reference": "746bce0fca664ac0a575e465f65c6643faddf7fb", "shasum": "" }, "require": { @@ -2721,7 +2187,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -2751,20 +2217,20 @@ "polyfill", "shim" ], - "time": "2016-05-18 14:26:46" + "time": "2016-11-14T01:06:16+00:00" }, { "name": "symfony/process", - "version": "v3.1.5", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "66de154ae86b1a07001da9fbffd620206e4faf94" + "reference": "32646a7cf53f3956c76dcb5c82555224ae321858" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/66de154ae86b1a07001da9fbffd620206e4faf94", - "reference": "66de154ae86b1a07001da9fbffd620206e4faf94", + "url": "https://api.github.com/repos/symfony/process/zipball/32646a7cf53f3956c76dcb5c82555224ae321858", + "reference": "32646a7cf53f3956c76dcb5c82555224ae321858", "shasum": "" }, "require": { @@ -2773,7 +2239,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2800,20 +2266,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2016-09-29 14:13:09" + "time": "2017-02-03T12:11:38+00:00" }, { "name": "symfony/routing", - "version": "v3.1.5", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8edf62498a1a4c57ba317664a4b698339c10cdf6" + "reference": "af464432c177dbcdbb32295113b7627500331f2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8edf62498a1a4c57ba317664a4b698339c10cdf6", - "reference": "8edf62498a1a4c57ba317664a4b698339c10cdf6", + "url": "https://api.github.com/repos/symfony/routing/zipball/af464432c177dbcdbb32295113b7627500331f2d", + "reference": "af464432c177dbcdbb32295113b7627500331f2d", "shasum": "" }, "require": { @@ -2842,7 +2308,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2875,20 +2341,20 @@ "uri", "url" ], - "time": "2016-08-16 14:58:24" + "time": "2017-01-28T02:37:08+00:00" }, { "name": "symfony/translation", - "version": "v3.1.5", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "93013a18d272e59dab8e67f583155b78c68947eb" + "reference": "ca032cc56976d88b85e7386b17020bc6dc95dbc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/93013a18d272e59dab8e67f583155b78c68947eb", - "reference": "93013a18d272e59dab8e67f583155b78c68947eb", + "url": "https://api.github.com/repos/symfony/translation/zipball/ca032cc56976d88b85e7386b17020bc6dc95dbc5", + "reference": "ca032cc56976d88b85e7386b17020bc6dc95dbc5", "shasum": "" }, "require": { @@ -2912,7 +2378,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2939,20 +2405,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2016-09-06 11:02:40" + "time": "2017-01-21T17:06:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v3.1.5", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "70bfe927b86ba9999aeebd829715b0bb2cd39a10" + "reference": "5bb4435a03a4f05c211f4a9a8ee2756965924511" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/70bfe927b86ba9999aeebd829715b0bb2cd39a10", - "reference": "70bfe927b86ba9999aeebd829715b0bb2cd39a10", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5bb4435a03a4f05c211f4a9a8ee2756965924511", + "reference": "5bb4435a03a4f05c211f4a9a8ee2756965924511", "shasum": "" }, "require": { @@ -2968,7 +2434,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -3002,20 +2468,67 @@ "debug", "dump" ], - "time": "2016-09-29 14:13:09" + "time": "2017-01-24T12:58:58+00:00" }, { - "name": "twig/twig", - "version": "v1.26.1", + "name": "tijsverkoyen/css-to-inline-styles", + "version": "2.2.0", "source": { "type": "git", - "url": "https://github.com/twigphp/Twig.git", - "reference": "a09d8ee17ac1cfea29ed60c83960ad685c6a898d" + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/a09d8ee17ac1cfea29ed60c83960ad685c6a898d", - "reference": "a09d8ee17ac1cfea29ed60c83960ad685c6a898d", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b", + "reference": "ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7", + "symfony/css-selector": "^2.7|~3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8|5.1.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "time": "2016-09-20T12:50:39+00:00" + }, + { + "name": "twig/twig", + "version": "v1.30.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "c6ff71094fde15d12398eaba029434b013dc5e59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/c6ff71094fde15d12398eaba029434b013dc5e59", + "reference": "c6ff71094fde15d12398eaba029434b013dc5e59", "shasum": "" }, "require": { @@ -3023,12 +2536,12 @@ }, "require-dev": { "symfony/debug": "~2.7", - "symfony/phpunit-bridge": "~2.7" + "symfony/phpunit-bridge": "~3.2@dev" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.26-dev" + "dev-master": "1.30-dev" } }, "autoload": { @@ -3063,7 +2576,7 @@ "keywords": [ "templating" ], - "time": "2016-10-05 18:57:41" + "time": "2016-12-23T11:06:22+00:00" }, { "name": "vlucas/phpdotenv", @@ -3113,20 +2626,20 @@ "env", "environment" ], - "time": "2016-09-01 10:05:43" + "time": "2016-09-01T10:05:43+00:00" }, { "name": "watson/validating", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/dwightwatson/validating.git", - "reference": "8ae5915976ddc152da54efcc03240d17370a60c3" + "reference": "3cef5b4cd0af2dc26d2c7ca668bd12f4d4ab421b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dwightwatson/validating/zipball/8ae5915976ddc152da54efcc03240d17370a60c3", - "reference": "8ae5915976ddc152da54efcc03240d17370a60c3", + "url": "https://api.github.com/repos/dwightwatson/validating/zipball/3cef5b4cd0af2dc26d2c7ca668bd12f4d4ab421b", + "reference": "3cef5b4cd0af2dc26d2c7ca668bd12f4d4ab421b", "shasum": "" }, "require": { @@ -3142,11 +2655,6 @@ "phpunit/phpunit": "~4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, "autoload": { "psr-4": { "Watson\\Validating\\": "src/" @@ -3168,10 +2676,179 @@ "laravel", "validation" ], - "time": "2016-08-28 13:39:22" + "time": "2016-10-31T21:53:17+00:00" } ], "packages-dev": [ + { + "name": "barryvdh/laravel-debugbar", + "version": "v2.3.2", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "24e4f0261e352d3fd86d0447791b56ae49398674" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/24e4f0261e352d3fd86d0447791b56ae49398674", + "reference": "24e4f0261e352d3fd86d0447791b56ae49398674", + "shasum": "" + }, + "require": { + "illuminate/support": "5.1.*|5.2.*|5.3.*|5.4.*", + "maximebf/debugbar": "~1.13.0", + "php": ">=5.5.9", + "symfony/finder": "~2.7|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" + ], + "time": "2017-01-19T08:19:49+00:00" + }, + { + "name": "barryvdh/laravel-ide-helper", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-ide-helper.git", + "reference": "555d3e37009bdb78f5d8bcea6eb8a816529a5cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/555d3e37009bdb78f5d8bcea6eb8a816529a5cfa", + "reference": "555d3e37009bdb78f5d8bcea6eb8a816529a5cfa", + "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", + "php": ">=5.4.0", + "symfony/class-loader": "^2.3|^3.0" + }, + "require-dev": { + "doctrine/dbal": "~2.3", + "phpunit/phpunit": "4.*", + "scrutinizer/ocular": "~1.1", + "squizlabs/php_codesniffer": "~2.3" + }, + "suggest": { + "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\LaravelIdeHelper\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.", + "keywords": [ + "autocomplete", + "codeintel", + "helper", + "ide", + "laravel", + "netbeans", + "phpdoc", + "phpstorm", + "sublime" + ], + "time": "2017-02-13T19:20:12+00:00" + }, + { + "name": "barryvdh/reflection-docblock", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/ReflectionDocBlock.git", + "reference": "3dcbd98b5d9384a5357266efba8fd29884458e5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/3dcbd98b5d9384a5357266efba8fd29884458e5c", + "reference": "3dcbd98b5d9384a5357266efba8fd29884458e5c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0,<4.5" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Barryvdh": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2016-06-13T19:28:20+00:00" + }, { "name": "doctrine/instantiator", "version": "1.0.5", @@ -3224,7 +2901,7 @@ "constructor", "instantiate" ], - "time": "2015-06-14 21:17:01" + "time": "2015-06-14T21:17:01+00:00" }, { "name": "fzaninotto/faker", @@ -3272,7 +2949,7 @@ "faker", "fixtures" ], - "time": "2016-04-29 12:21:54" + "time": "2016-04-29T12:21:54+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -3317,20 +2994,81 @@ "keywords": [ "test" ], - "time": "2015-05-11 14:41:42" + "time": "2015-05-11T14:41:42+00:00" }, { - "name": "mockery/mockery", - "version": "0.9.5", + "name": "maximebf/debugbar", + "version": "1.13.1", "source": { "type": "git", - "url": "https://github.com/padraic/mockery.git", - "reference": "4db079511a283e5aba1b3c2fb19037c645e70fc2" + "url": "https://github.com/maximebf/php-debugbar.git", + "reference": "afee79a236348e39a44cb837106b7c5b4897ac2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/padraic/mockery/zipball/4db079511a283e5aba1b3c2fb19037c645e70fc2", - "reference": "4db079511a283e5aba1b3c2fb19037c645e70fc2", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/afee79a236348e39a44cb837106b7c5b4897ac2a", + "reference": "afee79a236348e39a44cb837106b7c5b4897ac2a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "^1.0", + "symfony/var-dumper": "^2.6|^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0|^5.0" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", + "keywords": [ + "debug", + "debugbar" + ], + "time": "2017-01-05T08:46:19+00:00" + }, + { + "name": "mockery/mockery", + "version": "0.9.8", + "source": { + "type": "git", + "url": "https://github.com/padraic/mockery.git", + "reference": "1e5e2ffdc4d71d7358ed58a6fdd30a4a0c506855" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/padraic/mockery/zipball/1e5e2ffdc4d71d7358ed58a6fdd30a4a0c506855", + "reference": "1e5e2ffdc4d71d7358ed58a6fdd30a4a0c506855", "shasum": "" }, "require": { @@ -3382,20 +3120,20 @@ "test double", "testing" ], - "time": "2016-05-22 21:52:33" + "time": "2017-02-09T13:29:38+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.5.4", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f" + "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/ea74994a3dc7f8d2f65a06009348f2d63c81e61f", - "reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/5a5a9fc8025a08d8919be87d6884d5a92520cefe", + "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe", "shasum": "" }, "require": { @@ -3424,7 +3162,7 @@ "object", "object graph" ], - "time": "2016-09-16 13:37:59" + "time": "2017-01-26T22:05:40+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -3478,7 +3216,7 @@ "reflection", "static analysis" ], - "time": "2015-12-27 11:43:31" + "time": "2015-12-27T11:43:31+00:00" }, { "name": "phpdocumentor/reflection-docblock", @@ -3523,20 +3261,20 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2016-09-30 07:12:33" + "time": "2016-09-30T07:12:33+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.2", + "version": "0.2.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443" + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b39c7a5b194f9ed7bd0dd345c751007a41862443", - "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", "shasum": "" }, "require": { @@ -3570,20 +3308,20 @@ "email": "me@mikevanriel.com" } ], - "time": "2016-06-10 07:14:17" + "time": "2016-11-25T06:54:22+00:00" }, { "name": "phpspec/prophecy", - "version": "v1.6.1", + "version": "v1.6.2", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "58a8137754bc24b25740d4281399a4a3596058e0" + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/58a8137754bc24b25740d4281399a4a3596058e0", - "reference": "58a8137754bc24b25740d4281399a4a3596058e0", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6c52c2722f8460122f96f86346600e1077ce22cb", + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb", "shasum": "" }, "require": { @@ -3591,10 +3329,11 @@ "php": "^5.3|^7.0", "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", "sebastian/comparator": "^1.1", - "sebastian/recursion-context": "^1.0" + "sebastian/recursion-context": "^1.0|^2.0" }, "require-dev": { - "phpspec/phpspec": "^2.0" + "phpspec/phpspec": "^2.0", + "phpunit/phpunit": "^4.8 || ^5.6.5" }, "type": "library", "extra": { @@ -3632,20 +3371,20 @@ "spy", "stub" ], - "time": "2016-06-07 08:13:47" + "time": "2016-11-21T14:58:47+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "4.0.1", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3" + "reference": "c19cfc7cbb0e9338d8c469c7eedecc2a428b0971" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5f3f7e736d6319d5f1fc402aff8b026da26709a3", - "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c19cfc7cbb0e9338d8c469c7eedecc2a428b0971", + "reference": "c19cfc7cbb0e9338d8c469c7eedecc2a428b0971", "shasum": "" }, "require": { @@ -3695,20 +3434,20 @@ "testing", "xunit" ], - "time": "2016-07-26 14:39:29" + "time": "2017-01-20T15:06:43+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.1", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", - "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", "shasum": "" }, "require": { @@ -3742,7 +3481,7 @@ "filesystem", "iterator" ], - "time": "2015-06-21 13:08:43" + "time": "2016-10-03T07:40:28+00:00" }, { "name": "phpunit/php-text-template", @@ -3783,7 +3522,7 @@ "keywords": [ "template" ], - "time": "2015-06-21 13:50:34" + "time": "2015-06-21T13:50:34+00:00" }, { "name": "phpunit/php-timer", @@ -3827,20 +3566,20 @@ "keywords": [ "timer" ], - "time": "2016-05-12 18:03:57" + "time": "2016-05-12T18:03:57+00:00" }, { "name": "phpunit/php-token-stream", - "version": "1.4.8", + "version": "1.4.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" + "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", - "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3b402f65a4cc90abf6e1104e388b896ce209631b", + "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b", "shasum": "" }, "require": { @@ -3876,20 +3615,20 @@ "keywords": [ "tokenizer" ], - "time": "2015-09-15 10:49:45" + "time": "2016-11-15T14:06:22+00:00" }, { "name": "phpunit/phpunit", - "version": "5.6.1", + "version": "5.7.13", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "60c32c5b5e79c2248001efa2560f831da11cc2d7" + "reference": "60ebeed87a35ea46fd7f7d8029df2d6f013ebb34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/60c32c5b5e79c2248001efa2560f831da11cc2d7", - "reference": "60c32c5b5e79c2248001efa2560f831da11cc2d7", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/60ebeed87a35ea46fd7f7d8029df2d6f013ebb34", + "reference": "60ebeed87a35ea46fd7f7d8029df2d6f013ebb34", "shasum": "" }, "require": { @@ -3900,18 +3639,18 @@ "ext-xml": "*", "myclabs/deep-copy": "~1.3", "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "^4.0.1", + "phpspec/prophecy": "^1.6.2", + "phpunit/php-code-coverage": "^4.0.4", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", "phpunit/php-timer": "^1.0.6", "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "~1.1", + "sebastian/comparator": "^1.2.4", "sebastian/diff": "~1.2", - "sebastian/environment": "^1.3 || ^2.0", - "sebastian/exporter": "~1.2", - "sebastian/global-state": "~1.0", - "sebastian/object-enumerator": "~1.0", + "sebastian/environment": "^1.3.4 || ^2.0", + "sebastian/exporter": "~2.0", + "sebastian/global-state": "^1.1", + "sebastian/object-enumerator": "~2.0", "sebastian/resource-operations": "~1.0", "sebastian/version": "~1.0|~2.0", "symfony/yaml": "~2.1|~3.0" @@ -3932,7 +3671,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.6.x-dev" + "dev-master": "5.7.x-dev" } }, "autoload": { @@ -3958,27 +3697,27 @@ "testing", "xunit" ], - "time": "2016-10-07 13:03:26" + "time": "2017-02-10T09:05:10+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "3.4.0", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "238d7a2723bce689c79eeac9c7d5e1d623bb9dc2" + "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/238d7a2723bce689c79eeac9c7d5e1d623bb9dc2", - "reference": "238d7a2723bce689c79eeac9c7d5e1d623bb9dc2", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", + "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.6 || ^7.0", "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2" + "sebastian/exporter": "^1.2 || ^2.0" }, "conflict": { "phpunit/phpunit": "<5.4.0" @@ -4017,7 +3756,7 @@ "mock", "xunit" ], - "time": "2016-10-09 07:01:45" + "time": "2016-12-08T20:27:08+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -4062,26 +3801,26 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2016-02-13 06:45:14" + "time": "2016-02-13T06:45:14+00:00" }, { "name": "sebastian/comparator", - "version": "1.2.0", + "version": "1.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", - "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", "shasum": "" }, "require": { "php": ">=5.3.3", "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2" + "sebastian/exporter": "~1.2 || ~2.0" }, "require-dev": { "phpunit/phpunit": "~4.4" @@ -4126,7 +3865,7 @@ "compare", "equality" ], - "time": "2015-07-26 15:48:44" + "time": "2017-01-29T09:50:25+00:00" }, { "name": "sebastian/diff", @@ -4178,32 +3917,32 @@ "keywords": [ "diff" ], - "time": "2015-12-08 07:14:41" + "time": "2015-12-08T07:14:41+00:00" }, { "name": "sebastian/environment", - "version": "1.3.8", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.0" + "phpunit/phpunit": "^5.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -4228,25 +3967,25 @@ "environment", "hhvm" ], - "time": "2016-08-18 05:49:44" + "time": "2016-11-26T07:53:53+00:00" }, { "name": "sebastian/exporter", - "version": "1.2.2", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", "shasum": "" }, "require": { "php": ">=5.3.3", - "sebastian/recursion-context": "~1.0" + "sebastian/recursion-context": "~2.0" }, "require-dev": { "ext-mbstring": "*", @@ -4255,7 +3994,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -4295,7 +4034,7 @@ "export", "exporter" ], - "time": "2016-06-17 09:04:28" + "time": "2016-11-19T08:54:04+00:00" }, { "name": "sebastian/global-state", @@ -4346,25 +4085,25 @@ "keywords": [ "global state" ], - "time": "2015-10-12 03:26:01" + "time": "2015-10-12T03:26:01+00:00" }, { "name": "sebastian/object-enumerator", - "version": "1.0.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" + "reference": "96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", - "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35", + "reference": "96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35", "shasum": "" }, "require": { "php": ">=5.6", - "sebastian/recursion-context": "~1.0" + "sebastian/recursion-context": "~2.0" }, "require-dev": { "phpunit/phpunit": "~5" @@ -4372,7 +4111,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -4392,20 +4131,20 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2016-01-28 13:25:10" + "time": "2016-11-19T07:35:10+00:00" }, { "name": "sebastian/recursion-context", - "version": "1.0.2", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "913401df809e99e4f47b27cdd781f4a258d58791" + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", - "reference": "913401df809e99e4f47b27cdd781f4a258d58791", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", "shasum": "" }, "require": { @@ -4417,7 +4156,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -4445,7 +4184,7 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2015-11-11 19:50:13" + "time": "2016-11-19T07:33:16+00:00" }, { "name": "sebastian/resource-operations", @@ -4487,20 +4226,20 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28 20:34:47" + "time": "2015-07-28T20:34:47+00:00" }, { "name": "sebastian/version", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5" + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", - "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", "shasum": "" }, "require": { @@ -4530,34 +4269,41 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-02-04 12:56:52" + "time": "2016-10-03T07:35:21+00:00" }, { - "name": "symfony/css-selector", - "version": "v3.1.5", + "name": "symfony/class-loader", + "version": "v3.2.3", "source": { "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "ca809c64072e0fe61c1c7fb3c76cdc32265042ac" + "url": "https://github.com/symfony/class-loader.git", + "reference": "2847d56f518ad5721bf85aa9174b3aa3fd12aa03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/ca809c64072e0fe61c1c7fb3c76cdc32265042ac", - "reference": "ca809c64072e0fe61c1c7fb3c76cdc32265042ac", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/2847d56f518ad5721bf85aa9174b3aa3fd12aa03", + "reference": "2847d56f518ad5721bf85aa9174b3aa3fd12aa03", "shasum": "" }, "require": { "php": ">=5.5.9" }, + "require-dev": { + "symfony/finder": "~2.8|~3.0", + "symfony/polyfill-apcu": "~1.1" + }, + "suggest": { + "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\CssSelector\\": "" + "Symfony\\Component\\ClassLoader\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -4568,10 +4314,6 @@ "MIT" ], "authors": [ - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, { "name": "Fabien Potencier", "email": "fabien@symfony.com" @@ -4581,22 +4323,22 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony CssSelector Component", + "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2016-09-06 11:02:40" + "time": "2017-01-21T17:06:35+00:00" }, { "name": "symfony/dom-crawler", - "version": "v3.1.5", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "bb7395e8b1db3654de82b9f35d019958276de4d7" + "reference": "7eede2a901a19928494194f7d1815a77b9a473a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/bb7395e8b1db3654de82b9f35d019958276de4d7", - "reference": "bb7395e8b1db3654de82b9f35d019958276de4d7", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7eede2a901a19928494194f7d1815a77b9a473a0", + "reference": "7eede2a901a19928494194f7d1815a77b9a473a0", "shasum": "" }, "require": { @@ -4639,29 +4381,35 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2016-08-05 08:37:39" + "time": "2017-01-21T17:13:55+00:00" }, { "name": "symfony/yaml", - "version": "v3.1.5", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "368b9738d4033c8b93454cb0dbd45d305135a6d3" + "reference": "e1718c6bf57e1efbb8793ada951584b2ab27775b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/368b9738d4033c8b93454cb0dbd45d305135a6d3", - "reference": "368b9738d4033c8b93454cb0dbd45d305135a6d3", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e1718c6bf57e1efbb8793ada951584b2ab27775b", + "reference": "e1718c6bf57e1efbb8793ada951584b2ab27775b", "shasum": "" }, "require": { "php": ">=5.5.9" }, + "require-dev": { + "symfony/console": "~2.8|~3.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -4688,24 +4436,24 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-09-25 08:27:07" + "time": "2017-01-21T17:06:35+00:00" }, { "name": "webmozart/assert", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "bb2d123231c095735130cc8f6d31385a44c7b308" + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/bb2d123231c095735130cc8f6d31385a44c7b308", - "reference": "bb2d123231c095735130cc8f6d31385a44c7b308", + "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", "shasum": "" }, "require": { - "php": "^5.3.3|^7.0" + "php": "^5.3.3 || ^7.0" }, "require-dev": { "phpunit/phpunit": "^4.6", @@ -4714,7 +4462,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -4738,7 +4486,7 @@ "check", "validate" ], - "time": "2016-08-09 15:02:57" + "time": "2016-11-23T20:04:58+00:00" } ], "aliases": [], diff --git a/config/app.php b/config/app.php old mode 100755 new mode 100644 index 0611c68b7f..f92541fb61 --- a/config/app.php +++ b/config/app.php @@ -12,145 +12,22 @@ declare(strict_types = 1); return [ - - /* - |-------------------------------------------------------------------------- - | Application Name - |-------------------------------------------------------------------------- - | - | This value is the name of your application. This value is used when the - | framework needs to place the application's name in a notification or - | any other location as required by the application or its packages. - */ - - 'name' => 'Firefly III', - - /* - |-------------------------------------------------------------------------- - | Application Environment - |-------------------------------------------------------------------------- - | - | This value determines the "environment" your application is currently - | running in. This may determine how you prefer to configure various - | services your application utilizes. Set this in your ".env" file. - | - */ - - 'env' => env('APP_ENV', 'production'), - - /* - |-------------------------------------------------------------------------- - | Application Debug Mode - |-------------------------------------------------------------------------- - | - | When your application is in debug mode, detailed error messages with - | stack traces will be shown on every error that occurs within your - | application. If disabled, a simple generic error page is shown. - | - */ - - 'debug' => env('APP_DEBUG', false), - - /* - |-------------------------------------------------------------------------- - | Application URL - |-------------------------------------------------------------------------- - | - | This URL is used by the console to properly generate URLs when using - | the Artisan command line tool. You should set this to the root of - | your application so that it is used when running Artisan tasks. - | - */ - - 'url' => env('APP_URL', 'http://localhost'), - - /* - |-------------------------------------------------------------------------- - | Application Timezone - |-------------------------------------------------------------------------- - | - | Here you may specify the default timezone for your application, which - | will be used by the PHP date and date-time functions. We have gone - | ahead and set this to a sensible default for you out of the box. - | - */ - - 'timezone' => 'UTC', - - /* - |-------------------------------------------------------------------------- - | Application Locale Configuration - |-------------------------------------------------------------------------- - | - | The application locale determines the default locale that will be used - | by the translation service provider. You are free to set this value - | to any of the locales which will be supported by the application. - | - */ - - 'locale' => 'en_US', - - /* - |-------------------------------------------------------------------------- - | Application Fallback Locale - |-------------------------------------------------------------------------- - | - | The fallback locale determines the locale to use when the current one - | is not available. You may change the value to correspond to any of - | the language folders that are provided through your application. - | - */ - + 'name' => 'Firefly III', + 'env' => env('APP_ENV', 'production'), + 'debug' => env('APP_DEBUG', false), + 'url' => env('APP_URL', 'http://localhost'), + 'timezone' => 'UTC', + 'locale' => 'en_US', 'fallback_locale' => 'en_US', - - /* - |-------------------------------------------------------------------------- - | Encryption Key - |-------------------------------------------------------------------------- - | - | This key is used by the Illuminate encrypter service and should be set - | to a random, 32 character string, otherwise these encrypted strings - | will not be safe. Please do this before deploying an application! - | - */ - - 'key' => env('APP_KEY'), - - 'cipher' => 'AES-256-CBC', - - /* - |-------------------------------------------------------------------------- - | Logging Configuration - |-------------------------------------------------------------------------- - | - | Here you may configure the log settings for your application. Out of - | the box, Laravel uses the Monolog PHP logging library. This gives - | you a variety of powerful log handlers / formatters to utilize. - | - | Available Settings: "single", "daily", "syslog", "errorlog" - | - */ - - 'log' => env('APP_LOG', 'daily'), - - 'log_level' => env('APP_LOG_LEVEL', 'info'), - - /* - |-------------------------------------------------------------------------- - | Autoloaded Service Providers - |-------------------------------------------------------------------------- - | - | The service providers listed here will be automatically loaded on the - | request to your application. Feel free to add your own services to - | this array to grant expanded functionality to your applications. - | - */ - - 'providers' => [ + 'key' => env('APP_KEY'), + 'cipher' => 'AES-256-CBC', + 'log' => env('APP_LOG', 'daily'), + 'log_level' => env('APP_LOG_LEVEL', 'info'), + 'providers' => [ /* - * Laravel Framework Service Providers... - */ + * Laravel Framework Service Providers... + */ Illuminate\Auth\AuthServiceProvider::class, Illuminate\Broadcasting\BroadcastServiceProvider::class, Illuminate\Bus\BusServiceProvider::class, @@ -169,7 +46,7 @@ return [ Illuminate\Queue\QueueServiceProvider::class, Illuminate\Redis\RedisServiceProvider::class, Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, - Illuminate\Session\SessionServiceProvider::class, + FireflyIII\Providers\FireflySessionProvider::class, Illuminate\Translation\TranslationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, @@ -179,6 +56,7 @@ return [ /* * Application Service Providers... */ + FireflyIII\Providers\LogServiceProvider::class, FireflyIII\Providers\AppServiceProvider::class, FireflyIII\Providers\AuthServiceProvider::class, // FireflyIII\Providers\BroadcastServiceProvider::class, @@ -188,90 +66,78 @@ return [ // own stuff: -// Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class, -// Barryvdh\Debugbar\ServiceProvider::class, + //Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class, + //Barryvdh\Debugbar\ServiceProvider::class, DaveJamesMiller\Breadcrumbs\ServiceProvider::class, TwigBridge\ServiceProvider::class, 'PragmaRX\Google2FA\Vendor\Laravel\ServiceProvider', /* -* More service providers. -*/ - FireflyIII\Providers\CrudServiceProvider::class, + * More service providers. + */ FireflyIII\Providers\AccountServiceProvider::class, FireflyIII\Providers\AttachmentServiceProvider::class, FireflyIII\Providers\BillServiceProvider::class, FireflyIII\Providers\BudgetServiceProvider::class, FireflyIII\Providers\CategoryServiceProvider::class, + FireflyIII\Providers\CurrencyServiceProvider::class, FireflyIII\Providers\ExportJobServiceProvider::class, FireflyIII\Providers\JournalServiceProvider::class, FireflyIII\Providers\PiggyBankServiceProvider::class, FireflyIII\Providers\RuleServiceProvider::class, FireflyIII\Providers\RuleGroupServiceProvider::class, + FireflyIII\Providers\SearchServiceProvider::class, FireflyIII\Providers\TagServiceProvider::class, ], - - /* - |-------------------------------------------------------------------------- - | Class Aliases - |-------------------------------------------------------------------------- - | - | This array of class aliases will be registered when this application - | is started. However, feel free to register as many as you wish as - | the aliases are "lazy" loaded so they don't hinder performance. - | - */ - - 'aliases' => [ - - 'App' => Illuminate\Support\Facades\App::class, - 'Artisan' => Illuminate\Support\Facades\Artisan::class, - 'Auth' => Illuminate\Support\Facades\Auth::class, - 'Blade' => Illuminate\Support\Facades\Blade::class, - 'Cache' => Illuminate\Support\Facades\Cache::class, - 'Config' => Illuminate\Support\Facades\Config::class, - 'Cookie' => Illuminate\Support\Facades\Cookie::class, - 'Crypt' => Illuminate\Support\Facades\Crypt::class, - 'DB' => Illuminate\Support\Facades\DB::class, - 'Eloquent' => Illuminate\Database\Eloquent\Model::class, - 'Event' => Illuminate\Support\Facades\Event::class, - 'File' => Illuminate\Support\Facades\File::class, - 'Gate' => Illuminate\Support\Facades\Gate::class, - 'Hash' => Illuminate\Support\Facades\Hash::class, - 'Lang' => Illuminate\Support\Facades\Lang::class, - 'Log' => Illuminate\Support\Facades\Log::class, - 'Mail' => Illuminate\Support\Facades\Mail::class, - 'Notification' => Illuminate\Support\Facades\Notification::class, - 'Password' => Illuminate\Support\Facades\Password::class, - 'Queue' => Illuminate\Support\Facades\Queue::class, - 'Redirect' => Illuminate\Support\Facades\Redirect::class, - 'Redis' => Illuminate\Support\Facades\Redis::class, - 'Request' => Illuminate\Support\Facades\Request::class, - 'Response' => Illuminate\Support\Facades\Response::class, - 'Route' => Illuminate\Support\Facades\Route::class, - 'Schema' => Illuminate\Support\Facades\Schema::class, - 'Session' => Illuminate\Support\Facades\Session::class, - 'Storage' => Illuminate\Support\Facades\Storage::class, - 'URL' => Illuminate\Support\Facades\URL::class, - 'Validator' => Illuminate\Support\Facades\Validator::class, - 'View' => Illuminate\Support\Facades\View::class, - 'Twig' => 'TwigBridge\Facade\Twig', - 'Form' => Collective\Html\FormFacade::class, - 'Html' => Collective\Html\HtmlFacade::class, - 'Breadcrumbs' => 'DaveJamesMiller\Breadcrumbs\Facade', - 'Preferences' => 'FireflyIII\Support\Facades\Preferences', - 'FireflyConfig' => 'FireflyIII\Support\Facades\FireflyConfig', - 'Navigation' => 'FireflyIII\Support\Facades\Navigation', - 'Amount' => 'FireflyIII\Support\Facades\Amount', - 'Steam' => 'FireflyIII\Support\Facades\Steam', - 'ExpandedForm' => 'FireflyIII\Support\Facades\ExpandedForm', - 'Entrust' => 'Zizaco\Entrust\EntrustFacade', - 'Input' => 'Illuminate\Support\Facades\Input', - 'Google2FA' => 'PragmaRX\Google2FA\Vendor\Laravel\Facade', - - + 'aliases' => [ + 'App' => Illuminate\Support\Facades\App::class, + 'Artisan' => Illuminate\Support\Facades\Artisan::class, + 'Auth' => Illuminate\Support\Facades\Auth::class, + 'Blade' => Illuminate\Support\Facades\Blade::class, + 'Broadcast' => Illuminate\Support\Facades\Broadcast::class, + 'Bus' => Illuminate\Support\Facades\Bus::class, + 'Cache' => Illuminate\Support\Facades\Cache::class, + 'Config' => Illuminate\Support\Facades\Config::class, + 'Cookie' => Illuminate\Support\Facades\Cookie::class, + 'Crypt' => Illuminate\Support\Facades\Crypt::class, + 'DB' => Illuminate\Support\Facades\DB::class, + 'Eloquent' => Illuminate\Database\Eloquent\Model::class, + 'Event' => Illuminate\Support\Facades\Event::class, + 'File' => Illuminate\Support\Facades\File::class, + 'Gate' => Illuminate\Support\Facades\Gate::class, + 'Hash' => Illuminate\Support\Facades\Hash::class, + 'Lang' => Illuminate\Support\Facades\Lang::class, + 'Log' => Illuminate\Support\Facades\Log::class, + 'Mail' => Illuminate\Support\Facades\Mail::class, + 'Notification' => Illuminate\Support\Facades\Notification::class, + 'Password' => Illuminate\Support\Facades\Password::class, + 'Queue' => Illuminate\Support\Facades\Queue::class, + 'Redirect' => Illuminate\Support\Facades\Redirect::class, + 'Redis' => Illuminate\Support\Facades\Redis::class, + 'Request' => Illuminate\Support\Facades\Request::class, + 'Response' => Illuminate\Support\Facades\Response::class, + 'Route' => Illuminate\Support\Facades\Route::class, + 'Schema' => Illuminate\Support\Facades\Schema::class, + 'Session' => Illuminate\Support\Facades\Session::class, + 'Storage' => Illuminate\Support\Facades\Storage::class, + 'URL' => Illuminate\Support\Facades\URL::class, + 'Validator' => Illuminate\Support\Facades\Validator::class, + 'View' => Illuminate\Support\Facades\View::class, + 'Twig' => 'TwigBridge\Facade\Twig', + 'Form' => Collective\Html\FormFacade::class, + 'Html' => Collective\Html\HtmlFacade::class, + 'Breadcrumbs' => 'DaveJamesMiller\Breadcrumbs\Facade', + 'Preferences' => 'FireflyIII\Support\Facades\Preferences', + 'FireflyConfig' => 'FireflyIII\Support\Facades\FireflyConfig', + 'Navigation' => 'FireflyIII\Support\Facades\Navigation', + 'Amount' => 'FireflyIII\Support\Facades\Amount', + 'Steam' => 'FireflyIII\Support\Facades\Steam', + 'ExpandedForm' => 'FireflyIII\Support\Facades\ExpandedForm', + 'Entrust' => 'Zizaco\Entrust\EntrustFacade', + 'Input' => 'Illuminate\Support\Facades\Input', + 'Google2FA' => 'PragmaRX\Google2FA\Vendor\Laravel\Facade', ], ]; diff --git a/config/auth.php b/config/auth.php old mode 100755 new mode 100644 index b7e3177e63..55a6840414 --- a/config/auth.php +++ b/config/auth.php @@ -10,40 +10,10 @@ */ return [ - - /* - |-------------------------------------------------------------------------- - | Authentication Defaults - |-------------------------------------------------------------------------- - | - | This option controls the default authentication "guard" and password - | reset options for your application. You may change these defaults - | as required, but they're a perfect start for most applications. - | - */ - 'defaults' => [ 'guard' => 'web', 'passwords' => 'users', ], - - /* - |-------------------------------------------------------------------------- - | Authentication Guards - |-------------------------------------------------------------------------- - | - | Next, you may define every authentication guard for your application. - | Of course, a great default configuration has been defined for you - | here which uses session storage and the Eloquent user provider. - | - | All authentication drivers have a user provider. This defines how the - | users are actually retrieved out of your database or other storage - | mechanisms used by this application to persist your user's data. - | - | Supported: "session", "token" - | - */ - 'guards' => [ 'web' => [ 'driver' => 'session', @@ -55,55 +25,17 @@ return [ 'provider' => 'users', ], ], - - /* - |-------------------------------------------------------------------------- - | User Providers - |-------------------------------------------------------------------------- - | - | All authentication drivers have a user provider. This defines how the - | users are actually retrieved out of your database or other storage - | mechanisms used by this application to persist your user's data. - | - | If you have multiple user tables or models you may configure multiple - | sources which represent each model / table. These sources may then - | be assigned to any extra authentication guards you have defined. - | - | Supported: "database", "eloquent" - | - */ - 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => FireflyIII\User::class, ], ], - - /* - |-------------------------------------------------------------------------- - | Resetting Passwords - |-------------------------------------------------------------------------- - | - | Here you may set the options for resetting passwords including the view - | that is your password reset e-mail. You may also set the name of the - | table that maintains all of the reset tokens for your application. - | - | You may specify multiple password reset configurations if you have more - | than one user table or model in the application and you want to have - | separate password reset settings based on the specific user types. - | - | The expire time is the number of minutes that the reset token should be - | considered valid. This security feature keeps tokens short-lived so - | they have less time to be guessed. You may change this as needed. - | - */ - 'passwords' => [ 'users' => [ 'provider' => 'users', 'table' => 'password_resets', - 'expire' => 60, + 'expire' => 120, ], ], diff --git a/config/broadcasting.php b/config/broadcasting.php old mode 100755 new mode 100644 diff --git a/config/cache.php b/config/cache.php old mode 100755 new mode 100644 index 14728ed330..b8bc5dc989 --- a/config/cache.php +++ b/config/cache.php @@ -48,36 +48,36 @@ return [ ], 'database' => [ - 'driver' => 'database', - 'table' => 'cache', + 'driver' => 'database', + 'table' => 'cache', 'connection' => null, ], 'file' => [ 'driver' => 'file', - 'path' => storage_path('framework/cache'), + 'path' => storage_path('framework/cache'), ], 'memcached' => [ - 'driver' => 'memcached', + 'driver' => 'memcached', 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), - 'sasl' => [ + 'sasl' => [ env('MEMCACHED_USERNAME'), env('MEMCACHED_PASSWORD'), ], - 'options' => [ + 'options' => [ ], - 'servers' => [ + 'servers' => [ [ - 'host' => env('MEMCACHED_HOST', '127.0.0.1'), - 'port' => env('MEMCACHED_PORT', 11211), + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), 'weight' => 100, ], ], ], 'redis' => [ - 'driver' => 'redis', + 'driver' => 'redis', 'connection' => 'default', ], @@ -94,6 +94,6 @@ return [ | */ - 'prefix' => 'firefly', + 'prefix' => env('CACHE_PREFIX', 'firefly'), ]; diff --git a/config/compile.php b/config/compile.php old mode 100755 new mode 100644 diff --git a/config/database.php b/config/database.php old mode 100755 new mode 100644 index 81a863d474..451ffd9741 --- a/config/database.php +++ b/config/database.php @@ -11,120 +11,51 @@ return [ - /* - |-------------------------------------------------------------------------- - | PDO Fetch Style - |-------------------------------------------------------------------------- - | - | By default, database results will be returned as instances of the PHP - | stdClass object; however, you may desire to retrieve records in an - | array format for simplicity. Here you can tweak the fetch style. - | - */ - - 'fetch' => PDO::FETCH_OBJ, - - /* - |-------------------------------------------------------------------------- - | Default Database Connection Name - |-------------------------------------------------------------------------- - | - | Here you may specify which of the database connections below you wish - | to use as your default connection for all database work. Of course - | you may use many connections at once using the Database library. - | - */ - - 'default' => env('DB_CONNECTION', 'mysql'), - - /* - |-------------------------------------------------------------------------- - | Database Connections - |-------------------------------------------------------------------------- - | - | Here are each of the database connections setup for your application. - | Of course, examples of configuring each database platform that is - | supported by Laravel is shown below to make development simple. - | - | - | All database work in Laravel is done through the PHP PDO facilities - | so make sure you have the driver for your particular database of - | choice installed on your machine before you begin development. - | - */ - + 'fetch' => PDO::FETCH_OBJ, + 'default' => env('DB_CONNECTION', 'mysql'), 'connections' => [ - 'sqlite' => [ - 'driver' => 'sqlite', - 'database' => env('DB_DATABASE', database_path('database.sqlite')), - 'prefix' => '', + 'driver' => 'sqlite', + 'database' => env('DB_DATABASE', storage_path('database/database.sqlite')), + 'prefix' => '', ], 'mysql' => [ - 'driver' => 'mysql', - 'host' => env('DB_HOST', 'localhost'), - 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', + 'driver' => 'mysql', + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', - 'prefix' => '', - 'strict' => true, - 'engine' => null, + 'prefix' => '', + 'strict' => true, + 'engine' => null, ], 'pgsql' => [ - 'driver' => 'pgsql', - 'host' => env('DB_HOST', 'localhost'), - 'port' => env('DB_PORT', '5432'), + 'driver' => 'pgsql', + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '5432'), 'database' => env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', - 'prefix' => '', - 'schema' => 'public', - 'sslmode' => 'prefer', + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + 'sslmode' => 'prefer', ], ], - - /* - |-------------------------------------------------------------------------- - | Migration Repository Table - |-------------------------------------------------------------------------- - | - | This table keeps track of all the migrations that have already run for - | your application. Using this information, we can determine which of - | the migrations on disk haven't actually been run in the database. - | - */ - - 'migrations' => 'migrations', - - /* - |-------------------------------------------------------------------------- - | Redis Databases - |-------------------------------------------------------------------------- - | - | Redis is an open source, fast, and advanced key-value store that also - | provides a richer set of commands than a typical key-value systems - | such as APC or Memcached. Laravel makes it easy to dig right in. - | - */ - - 'redis' => [ - + 'migrations' => 'migrations', + 'redis' => [ 'cluster' => false, - 'default' => [ - 'host' => env('REDIS_HOST', 'localhost'), + 'host' => env('REDIS_HOST', 'localhost'), 'password' => env('REDIS_PASSWORD', null), - 'port' => env('REDIS_PORT', 6379), + 'port' => env('REDIS_PORT', 6379), 'database' => 0, ], - ], - ]; diff --git a/config/filesystems.php b/config/filesystems.php old mode 100755 new mode 100644 index bd0b9738ac..ba8b6b6eb9 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -52,7 +52,7 @@ return [ 'disks' => [ - 'local' => [ + 'local' => [ 'driver' => 'local', 'root' => storage_path('app'), ], @@ -69,7 +69,14 @@ return [ 'driver' => 'local', 'root' => storage_path('database'), ], - + 'seeds' => [ + 'driver' => 'local', + 'root' => base_path('resources/seeds'), + ], + 'stubs' => [ + 'driver' => 'local', + 'root' => base_path('resources/stubs'), + ], 'public' => [ 'driver' => 'local', diff --git a/config/firefly.php b/config/firefly.php index be4ee78bfd..5ffebe0c43 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -20,9 +20,11 @@ declare(strict_types = 1); return [ 'configuration' => [ 'single_user_mode' => true, + 'is_demo_site' => false, ], + 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), 'chart' => 'chartjs', - 'version' => '4.1.4', + 'version' => '4.3.4', 'csv_import_enabled' => true, 'maxUploadSize' => 5242880, 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'], @@ -39,12 +41,7 @@ return [ 'default_export_format' => 'csv', 'default_import_format' => 'csv', 'bill_periods' => ['weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], - 'accountRoles' => [ - 'defaultAsset' => 'Default asset account', - 'sharedAsset' => 'Shared asset account', - 'savingAsset' => 'Savings account', - 'ccAsset' => 'Credit card', - ], + 'accountRoles' => ['defaultAsset', 'sharedAsset', 'savingAsset', 'ccAsset',], 'ccTypes' => [ 'monthlyFull' => 'Full payment every month', ], @@ -105,12 +102,15 @@ return [ 'Cash account' => 'cash', ], 'languages' => [ - 'de_DE' => ['name_locale' => 'Deutsch', 'name_english' => 'German', 'complete' => false], + 'de_DE' => ['name_locale' => 'Deutsch', 'name_english' => 'German', 'complete' => true], 'en_US' => ['name_locale' => 'English', 'name_english' => 'English', 'complete' => true], + 'es_ES' => ['name_locale' => 'Español', 'name_english' => 'Spanish', 'complete' => false], 'fr_FR' => ['name_locale' => 'Français', 'name_english' => 'French', 'complete' => false], 'hr_HR' => ['name_locale' => 'hrvatski', 'name_english' => 'Croatian', 'complete' => false], 'nl_NL' => ['name_locale' => 'Nederlands', 'name_english' => 'Dutch', 'complete' => true], + 'pl_PL' => ['name_locale' => 'Polski', 'name_english' => 'Polish ', 'complete' => false], 'pt_BR' => ['name_locale' => 'Português do Brasil', 'name_english' => 'Portuguese (Brazil)', 'complete' => true], + 'ru-RU' => ['name_locale' => 'Russian', 'name_english' => 'Russian', 'complete' => false], 'zh-HK' => ['name_locale' => '繁體中文(香港)', 'name_english' => 'Chinese Traditional, Hong Kong', 'complete' => false], 'zh-TW' => ['name_locale' => '正體中文', 'name_english' => 'Chinese Traditional', 'complete' => false], ], @@ -137,8 +137,10 @@ return [ 'bill' => 'FireflyIII\Models\Bill', 'budget' => 'FireflyIII\Models\Budget', 'category' => 'FireflyIII\Models\Category', + 'transaction_type' => 'FireflyIII\Models\TransactionType', 'currency' => 'FireflyIII\Models\TransactionCurrency', 'limitrepetition' => 'FireflyIII\Models\LimitRepetition', + 'budgetlimit' => 'FireflyIII\Models\BudgetLimit', 'piggyBank' => 'FireflyIII\Models\PiggyBank', 'tj' => 'FireflyIII\Models\TransactionJournal', 'unfinishedJournal' => 'FireflyIII\Support\Binder\UnfinishedJournal', @@ -151,6 +153,7 @@ return [ 'budgetList' => 'FireflyIII\Support\Binder\BudgetList', 'journalList' => 'FireflyIII\Support\Binder\JournalList', 'categoryList' => 'FireflyIII\Support\Binder\CategoryList', + 'tagList' => 'FireflyIII\Support\Binder\TagList', 'start_date' => 'FireflyIII\Support\Binder\Date', 'end_date' => 'FireflyIII\Support\Binder\Date', ], @@ -164,7 +167,6 @@ return [ 'to_account_ends' => 'FireflyIII\Rules\Triggers\ToAccountEnds', 'to_account_is' => 'FireflyIII\Rules\Triggers\ToAccountIs', 'to_account_contains' => 'FireflyIII\Rules\Triggers\ToAccountContains', - 'transaction_type' => 'FireflyIII\Rules\Triggers\TransactionType', 'amount_less' => 'FireflyIII\Rules\Triggers\AmountLess', 'amount_exactly' => 'FireflyIII\Rules\Triggers\AmountExactly', 'amount_more' => 'FireflyIII\Rules\Triggers\AmountMore', @@ -172,6 +174,10 @@ return [ 'description_ends' => 'FireflyIII\Rules\Triggers\DescriptionEnds', 'description_contains' => 'FireflyIII\Rules\Triggers\DescriptionContains', 'description_is' => 'FireflyIII\Rules\Triggers\DescriptionIs', + 'transaction_type' => 'FireflyIII\Rules\Triggers\TransactionType', + 'category_is' => 'FireflyIII\Rules\Triggers\CategoryIs', + 'budget_is' => 'FireflyIII\Rules\Triggers\BudgetIs', + 'tag_is' => 'FireflyIII\Rules\Triggers\TagIs', ], 'rule-actions' => [ 'set_category' => 'FireflyIII\Rules\Actions\SetCategory', @@ -184,6 +190,9 @@ return [ 'set_description' => 'FireflyIII\Rules\Actions\SetDescription', 'append_description' => 'FireflyIII\Rules\Actions\AppendDescription', 'prepend_description' => 'FireflyIII\Rules\Actions\PrependDescription', + + 'set_source_account' => 'FireflyIII\Rules\Actions\SetSourceAccount', + 'set_destination_account' => 'FireflyIII\Rules\Actions\SetDestinationAccount', ], 'rule-actions-text' => [ 'set_category', @@ -200,5 +209,4 @@ return [ ], 'default_currency' => 'EUR', 'default_language' => 'en_US', - 'show-demo-warning' => false, ]; diff --git a/config/mail.php b/config/mail.php old mode 100755 new mode 100644 diff --git a/config/queue.php b/config/queue.php old mode 100755 new mode 100644 diff --git a/config/services.php b/config/services.php old mode 100755 new mode 100644 diff --git a/config/session.php b/config/session.php old mode 100755 new mode 100644 index 51698cb45e..f5af7f0fdc --- a/config/session.php +++ b/config/session.php @@ -13,179 +13,18 @@ declare(strict_types = 1); return [ - - /* - |-------------------------------------------------------------------------- - | Default Session Driver - |-------------------------------------------------------------------------- - | - | This option controls the default session "driver" that will be used on - | requests. By default, we will use the lightweight native driver but - | you may specify any of the other wonderful drivers provided here. - | - | Supported: "file", "cookie", "database", "apc", - | "memcached", "redis", "array" - | - */ - - 'driver' => env('SESSION_DRIVER', 'file'), - - /* - |-------------------------------------------------------------------------- - | Session Lifetime - |-------------------------------------------------------------------------- - | - | Here you may specify the number of minutes that you wish the session - | to be allowed to remain idle before it expires. If you want them - | to immediately expire on the browser closing, set that option. - | - */ - - 'lifetime' => 120, - + 'driver' => env('SESSION_DRIVER', 'file'), + 'lifetime' => 120, 'expire_on_close' => false, - - /* - |-------------------------------------------------------------------------- - | Session Encryption - |-------------------------------------------------------------------------- - | - | This option allows you to easily specify that all of your session data - | should be encrypted before it is stored. All encryption will be run - | automatically by Laravel and you can use the Session like normal. - | - */ - - 'encrypt' => true, - - /* - |-------------------------------------------------------------------------- - | Session File Location - |-------------------------------------------------------------------------- - | - | When using the native session driver, we need a location where session - | files may be stored. A default has been set for you but a different - | location may be specified. This is only needed for file sessions. - | - */ - - 'files' => storage_path('framework/sessions'), - - /* - |-------------------------------------------------------------------------- - | Session Database Connection - |-------------------------------------------------------------------------- - | - | When using the "database" or "redis" session drivers, you may specify a - | connection that should be used to manage these sessions. This should - | correspond to a connection in your database configuration options. - | - */ - - 'connection' => null, - - /* - |-------------------------------------------------------------------------- - | Session Database Table - |-------------------------------------------------------------------------- - | - | When using the "database" session driver, you may specify the table we - | should use to manage the sessions. Of course, a sensible default is - | provided for you; however, you are free to change this as needed. - | - */ - - 'table' => 'sessions', - - /* - |-------------------------------------------------------------------------- - | Session Cache Store - |-------------------------------------------------------------------------- - | - | When using the "apc" or "memcached" session drivers, you may specify a - | cache store that should be used for these sessions. This value must - | correspond with one of the application's configured cache stores. - | - */ - - 'store' => null, - - /* - |-------------------------------------------------------------------------- - | Session Sweeping Lottery - |-------------------------------------------------------------------------- - | - | Some session drivers must manually sweep their storage location to get - | rid of old sessions from storage. Here are the chances that it will - | happen on a given request. By default, the odds are 2 out of 100. - | - */ - - 'lottery' => [2, 100], - - /* - |-------------------------------------------------------------------------- - | Session Cookie Name - |-------------------------------------------------------------------------- - | - | Here you may change the name of the cookie used to identify a session - | instance by ID. The name specified here will get used every time a - | new session cookie is created by the framework for every driver. - | - */ - - 'cookie' => 'firefly_session', - - /* - |-------------------------------------------------------------------------- - | Session Cookie Path - |-------------------------------------------------------------------------- - | - | The session cookie path determines the path for which the cookie will - | be regarded as available. Typically, this will be the root path of - | your application but you are free to change this when necessary. - | - */ - - 'path' => env('COOKIE_PATH', '/'), - - /* - |-------------------------------------------------------------------------- - | Session Cookie Domain - |-------------------------------------------------------------------------- - | - | Here you may change the domain of the cookie used to identify a session - | in your application. This will determine which domains the cookie is - | available to in your application. A sensible default has been set. - | - */ - - 'domain' => env('COOKIE_DOMAIN', null), - - /* - |-------------------------------------------------------------------------- - | HTTPS Only Cookies - |-------------------------------------------------------------------------- - | - | By setting this option to true, session cookies will only be sent back - | to the server if the browser has a HTTPS connection. This will keep - | the cookie from being sent to you if it can not be done securely. - | - */ - - 'secure' => env('COOKIE_SECURE', false), - - /* - |-------------------------------------------------------------------------- - | HTTP Access Only - |-------------------------------------------------------------------------- - | - | Setting this value to true will prevent JavaScript from accessing the - | value of the cookie and the cookie will only be accessible through - | the HTTP protocol. You are free to modify this option if needed. - | - */ - - 'http_only' => !env('COOKIE_SECURE', false), - + 'encrypt' => true, + 'files' => storage_path('framework/sessions'), + 'connection' => null, + 'table' => 'sessions', + 'store' => null, + 'lottery' => [2, 100], + 'cookie' => 'firefly_session', + 'path' => env('COOKIE_PATH', '/'), + 'domain' => env('COOKIE_DOMAIN', null), + 'secure' => env('COOKIE_SECURE', false), + 'http_only' => true, ]; diff --git a/config/twigbridge.php b/config/twigbridge.php index 886f44d6e1..dc51e30651 100644 --- a/config/twigbridge.php +++ b/config/twigbridge.php @@ -159,7 +159,7 @@ return [ 'ExpandedForm' => [ 'is_safe' => [ 'date', 'text', 'select', 'balance', 'optionsList', 'checkbox', 'amount', 'tags', 'integer', 'textarea', 'location', - 'multiRadio', 'file', 'multiCheckbox', 'staticText', 'amountSmall', + 'multiRadio', 'file', 'multiCheckbox', 'staticText', 'amountSmall', 'password', ], ], 'Form' => [ diff --git a/config/upgrade.php b/config/upgrade.php index 82a97f50a0..199596e690 100644 --- a/config/upgrade.php +++ b/config/upgrade.php @@ -11,23 +11,15 @@ declare(strict_types = 1); - -/** - * upgrade.php - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - - return [ 'text' => [ - '3.7' => 'Because of the upgrade to Laravel 5.2, several manual changes must be made to your Firefly III installation. ' . - 'Please follow the instructions on the following page: https://github.com/JC5/firefly-iii/wiki/Upgrade-to-3.7.0', - '3.8' => 'This version of Firefly III requires PHP 7.0.', - '3.10' => 'Please find the full upgrade instructions here: https://github.com/JC5/firefly-iii/wiki/Upgrade-to-3.10', - '4.0' => 'Please find the full upgrade instructions here: https://github.com/JC5/firefly-iii/wiki/Upgrade-to-4.0', - '4.1' => 'Please find the full upgrade instructions here: https://github.com/JC5/firefly-iii/wiki/Upgrade-to-4.0', + 'upgrade' => + [ + '4.3' => 'Make sure you run the migrations and clear your cache. If you need more help, please check Github or the Firefly III website.', + ], + 'install' => + [ + '4.3' => 'Welcome to Firefly! Make sure you follow the installation guide. If you need more help, please check Github or the Firefly III website. The installation guide has a FAQ which you should check out as well.', + ], ], ]; diff --git a/config/view.php b/config/view.php old mode 100755 new mode 100644 diff --git a/crowdin.yaml b/crowdin.yaml index 85cd4661c0..2ebe1dbabf 100644 --- a/crowdin.yaml +++ b/crowdin.yaml @@ -1,4 +1,4 @@ files: - source: /resources/lang/en_US/*.php - translation: /resources/lang/%locale_with_underscore%/ + translation: /resources/lang/%locale_with_underscore%/%original_file_name% diff --git a/database/.gitignore b/database/.gitignore old mode 100755 new mode 100644 diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php old mode 100755 new mode 100644 index c889b7df44..8a65c4ee73 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -26,7 +26,6 @@ $factory->define( static $password; return [ - 'name' => $faker->name, 'email' => $faker->safeEmail, 'password' => $password ?: $password = bcrypt('secret'), 'remember_token' => str_random(10), diff --git a/database/migrations/.gitkeep b/database/migrations/.gitkeep old mode 100755 new mode 100644 diff --git a/database/migrations/2016_06_16_000000_create_support_tables.php b/database/migrations/2016_06_16_000000_create_support_tables.php index 2ff3c650f8..6d0234802c 100644 --- a/database/migrations/2016_06_16_000000_create_support_tables.php +++ b/database/migrations/2016_06_16_000000_create_support_tables.php @@ -21,7 +21,6 @@ class CreateSupportTables extends Migration /** * Reverse the migrations. * - * @return void */ public function down() { @@ -42,56 +41,20 @@ class CreateSupportTables extends Migration /** * Run the migrations. * - * @return void + * @SuppressWarnings(PHPMD.ShortMethodName) */ public function up() { - /* - * account_types - */ $this->createAccountTypeTable(); - /* - * transaction_currencies - */ $this->createCurrencyTable(); - - /* - * transaction_types - */ $this->createTransactionTypeTable(); - - /* - * jobs - */ $this->createJobsTable(); - - /* - * password_resets - */ $this->createPasswordTable(); - - /* - * permissions - */ $this->createPermissionsTable(); - - /* - * roles - */ $this->createRolesTable(); - - /* - * permission_role - */ $this->createPermissionRoleTable(); - - /* - * sessions - */ $this->createSessionsTable(); - $this->createConfigurationTable(); - } /** diff --git a/database/migrations/2016_06_16_000001_create_users_table.php b/database/migrations/2016_06_16_000001_create_users_table.php index 4b3991bd67..0120a485ed 100644 --- a/database/migrations/2016_06_16_000001_create_users_table.php +++ b/database/migrations/2016_06_16_000001_create_users_table.php @@ -21,7 +21,7 @@ class CreateUsersTable extends Migration /** * Run the migrations. * - * @return void + * @SuppressWarnings(PHPMD.ShortMethodName) */ public function up() { @@ -43,8 +43,6 @@ class CreateUsersTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { 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 1d214c7d2e..1090bcb05f 100644 --- a/database/migrations/2016_06_16_000002_create_main_tables.php +++ b/database/migrations/2016_06_16_000002_create_main_tables.php @@ -55,6 +55,8 @@ class CreateMainTables extends Migration /** * Run the migrations. + * + * @SuppressWarnings(PHPMD.ShortMethodName) */ public function up() { @@ -87,16 +89,11 @@ class CreateMainTables extends Migration $table->integer('user_id', false, true); $table->integer('account_type_id', false, true); $table->string('name', 1024); - $table->decimal('virtual_balance', 10, 4)->nullable(); + $table->decimal('virtual_balance', 22, 12)->nullable(); $table->string('iban', 255)->nullable(); - $table->boolean('active')->default(1); $table->boolean('encrypted')->default(0); - - // link user id to users table $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); - - // link account type id to account types table $table->foreign('account_type_id')->references('id')->on('account_types')->onDelete('cascade'); } ); @@ -110,8 +107,6 @@ class CreateMainTables extends Migration $table->integer('account_id', false, true); $table->string('name'); $table->text('data'); - - // link account id to accounts: $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); } ); @@ -165,8 +160,8 @@ class CreateMainTables extends Migration $table->integer('user_id', false, true); $table->string('name', 1024); $table->string('match', 1024); - $table->decimal('amount_min', 10, 4); - $table->decimal('amount_max', 10, 4); + $table->decimal('amount_min', 22, 12); + $table->decimal('amount_max', 22, 12); $table->date('date'); $table->string('repeat_freq', 30); $table->smallInteger('skip', false, true)->default(0); @@ -183,12 +178,10 @@ class CreateMainTables extends Migration } /** - * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) // cannot be helped. */ private function createBudgetTables() { - - if (!Schema::hasTable('budgets')) { Schema::create( 'budgets', function (Blueprint $table) { @@ -199,8 +192,6 @@ class CreateMainTables extends Migration $table->string('name', 1024); $table->boolean('active')->default(1); $table->boolean('encrypted')->default(0); - - // link user id to users table $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); @@ -210,16 +201,13 @@ class CreateMainTables extends Migration if (!Schema::hasTable('budget_limits')) { Schema::create( 'budget_limits', function (Blueprint $table) { - $table->increments('id'); $table->timestamps(); $table->integer('budget_id', false, true); $table->date('startdate'); - $table->decimal('amount', 10, 4); + $table->decimal('amount', 22, 12); $table->string('repeat_freq', 30); $table->boolean('repeats')->default(0); - - // link budget id to budgets table $table->foreign('budget_id')->references('id')->on('budgets')->onDelete('cascade'); } @@ -233,9 +221,7 @@ class CreateMainTables extends Migration $table->integer('budget_limit_id', false, true); $table->date('startdate'); $table->date('enddate'); - $table->decimal('amount', 10, 4); - - // link budget limit id to budget_limitss table + $table->decimal('amount', 22, 12); $table->foreign('budget_limit_id')->references('id')->on('budget_limits')->onDelete('cascade'); } ); @@ -313,14 +299,12 @@ class CreateMainTables extends Migration $table->softDeletes(); $table->integer('account_id', false, true); $table->string('name', 1024); - $table->decimal('targetamount', 10, 4); + $table->decimal('targetamount', 22, 12); $table->date('startdate')->nullable(); $table->date('targetdate')->nullable(); $table->integer('order', false, true)->default(0); $table->boolean('active')->default(0); $table->boolean('encrypted')->default(1); - - // link to account_id to accounts $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); } ); @@ -334,14 +318,11 @@ class CreateMainTables extends Migration $table->integer('piggy_bank_id', false, true); $table->date('startdate')->nullable(); $table->date('targetdate')->nullable(); - $table->decimal('currentamount', 10, 4); - + $table->decimal('currentamount', 22, 12); $table->foreign('piggy_bank_id')->references('id')->on('piggy_banks')->onDelete('cascade'); - } ); } - } /** @@ -388,7 +369,8 @@ class CreateMainTables extends Migration } /** - * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) // cannot be helped. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // its exactly five */ private function createRuleTables() { @@ -490,8 +472,8 @@ class CreateMainTables extends Migration $table->string('tagMode', 1024); $table->date('date')->nullable(); $table->text('description')->nullable(); - $table->decimal('latitude', 18, 12)->nullable(); - $table->decimal('longitude', 18, 12)->nullable(); + $table->decimal('latitude', 24, 12)->nullable(); + $table->decimal('longitude', 24, 12)->nullable(); $table->boolean('zoomLevel')->nullable(); // link user id to users table @@ -503,7 +485,9 @@ class CreateMainTables extends Migration } /** - * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) // cannot be helped. + * @SuppressWarnings(PHPMD.NPathComplexity) // cannot be helped + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // its exactly five */ private function createTransactionTables() { @@ -513,26 +497,19 @@ class CreateMainTables extends Migration $table->increments('id'); $table->timestamps(); $table->softDeletes(); - $table->integer('user_id', false, true); $table->integer('transaction_type_id', false, true); $table->integer('bill_id', false, true)->nullable(); $table->integer('transaction_currency_id', false, true); - $table->string('description', 1024); - $table->date('date'); $table->date('interest_date')->nullable(); $table->date('book_date')->nullable(); $table->date('process_date')->nullable(); - $table->integer('order', false, true)->default(0); $table->integer('tag_count', false, true); - $table->boolean('encrypted')->default(1); $table->boolean('completed')->default(1); - - // links to other tables: $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('transaction_type_id')->references('id')->on('transaction_types')->onDelete('cascade'); $table->foreign('bill_id')->references('id')->on('bills')->onDelete('set null'); @@ -550,7 +527,6 @@ class CreateMainTables extends Migration $table->string('name', 255); $table->text('data'); $table->string('hash', 64); - $table->foreign('transaction_journal_id')->references('id')->on('transaction_journals')->onDelete('cascade'); } ); @@ -562,7 +538,6 @@ class CreateMainTables extends Migration $table->increments('id'); $table->integer('tag_id', false, true); $table->integer('transaction_journal_id', false, true); - $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); $table->foreign('transaction_journal_id')->references('id')->on('transaction_journals')->onDelete('cascade'); @@ -577,7 +552,6 @@ class CreateMainTables extends Migration $table->increments('id'); $table->integer('budget_id', false, true); $table->integer('transaction_journal_id', false, true); - $table->foreign('budget_id')->references('id')->on('budgets')->onDelete('cascade'); $table->foreign('transaction_journal_id')->references('id')->on('transaction_journals')->onDelete('cascade'); } @@ -590,7 +564,6 @@ class CreateMainTables extends Migration $table->increments('id'); $table->integer('category_id', false, true); $table->integer('transaction_journal_id', false, true); - $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade'); $table->foreign('transaction_journal_id')->references('id')->on('transaction_journals')->onDelete('cascade'); } @@ -606,7 +579,7 @@ class CreateMainTables extends Migration $table->integer('piggy_bank_id', false, true); $table->integer('transaction_journal_id', false, true)->nullable(); $table->date('date'); - $table->decimal('amount', 10, 4); + $table->decimal('amount', 22, 12); $table->foreign('piggy_bank_id')->references('id')->on('piggy_banks')->onDelete('cascade'); $table->foreign('transaction_journal_id')->references('id')->on('transaction_journals')->onDelete('set null'); @@ -623,7 +596,7 @@ class CreateMainTables extends Migration $table->integer('account_id', false, true); $table->integer('transaction_journal_id', false, true); $table->string('description', 1024)->nullable(); - $table->decimal('amount', 10, 4); + $table->decimal('amount', 22, 12); $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); $table->foreign('transaction_journal_id')->references('id')->on('transaction_journals')->onDelete('cascade'); diff --git a/database/migrations/2016_08_25_091522_changes_for_3101.php b/database/migrations/2016_08_25_091522_changes_for_3101.php index 24883b7933..9d0e38a548 100644 --- a/database/migrations/2016_08_25_091522_changes_for_3101.php +++ b/database/migrations/2016_08_25_091522_changes_for_3101.php @@ -20,8 +20,6 @@ class ChangesFor3101 extends Migration { /** * Reverse the migrations. - * - * @return void */ public function down() { @@ -31,13 +29,13 @@ class ChangesFor3101 extends Migration /** * Run the migrations. * - * @return void + * @SuppressWarnings(PHPMD.ShortMethodName) */ public function up() { Schema::table( 'import_jobs', function (Blueprint $table) { - $table->text('extended_status'); + $table->text('extended_status')->nullable(); } ); } diff --git a/database/migrations/2016_09_12_121359_fix_nullables.php b/database/migrations/2016_09_12_121359_fix_nullables.php index ff89047969..54033eac70 100644 --- a/database/migrations/2016_09_12_121359_fix_nullables.php +++ b/database/migrations/2016_09_12_121359_fix_nullables.php @@ -21,8 +21,6 @@ class FixNullables extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { @@ -32,7 +30,7 @@ class FixNullables extends Migration /** * Run the migrations. * - * @return void + * @SuppressWarnings(PHPMD.ShortMethodName) */ public function up() { diff --git a/database/migrations/2016_10_09_150037_expand_transactions_table.php b/database/migrations/2016_10_09_150037_expand_transactions_table.php index ca363bc451..fdb957ba94 100644 --- a/database/migrations/2016_10_09_150037_expand_transactions_table.php +++ b/database/migrations/2016_10_09_150037_expand_transactions_table.php @@ -1,8 +1,16 @@ softDeletes(); + } + ); + } +} diff --git a/database/migrations/2016_12_22_150431_changes_for_v430.php b/database/migrations/2016_12_22_150431_changes_for_v430.php new file mode 100644 index 0000000000..84f61fb4c1 --- /dev/null +++ b/database/migrations/2016_12_22_150431_changes_for_v430.php @@ -0,0 +1,43 @@ +increments('id'); + $table->timestamps(); + $table->softDeletes(); + $table->integer('user_id', false, true); + $table->integer('transaction_currency_id', false, true); + $table->decimal('amount', 22, 12); + $table->date('start_date'); + $table->date('end_date'); + + + $table->foreign('transaction_currency_id')->references('id')->on('transaction_currencies')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + } + ); + } +} diff --git a/database/migrations/2016_12_28_203205_changes_for_v431.php b/database/migrations/2016_12_28_203205_changes_for_v431.php new file mode 100644 index 0000000000..310ccac5a8 --- /dev/null +++ b/database/migrations/2016_12_28_203205_changes_for_v431.php @@ -0,0 +1,97 @@ +string('repeat_freq', 30)->nullable(); + } + ); + Schema::table( + 'budget_limits', function (Blueprint $table) { + $table->boolean('repeats')->default(0); + } + ); + + + // remove date field "end_date" + Schema::table( + 'budget_limits', function (Blueprint $table) { + $table->dropColumn('end_date'); + } + ); + + // change field "start_date" to "startdate" +// Schema::table( +// 'budget_limits', function (Blueprint $table) { +// $table->renameColumn('startdate', 'start_date'); +// } +// ); + + } + + /** + * Run the migrations. + * + * @SuppressWarnings(PHPMD.ShortMethodName) + */ + public function up() + { + // add decimal places to "transaction currencies". + Schema::table( + 'transaction_currencies', function (Blueprint $table) { + $table->smallInteger('decimal_places', false, true)->default(2); + } + ); + + // change field "startdate" to "start_date" + Schema::table( + 'budget_limits', function (Blueprint $table) { + $table->renameColumn('startdate', 'start_date'); + } + ); + + // add date field "end_date" after "start_date" + Schema::table( + 'budget_limits', function (Blueprint $table) { + $table->date('end_date')->nullable()->after('start_date'); + } + ); + + // drop "repeats" and "repeat_freq". + Schema::table( + 'budget_limits', function (Blueprint $table) { + $table->dropColumn('repeats'); + + } + ); + Schema::table( + 'budget_limits', function (Blueprint $table) { + $table->dropColumn('repeat_freq'); + } + ); + + + } +} diff --git a/database/seeds/.gitkeep b/database/seeds/.gitkeep old mode 100755 new mode 100644 diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php old mode 100755 new mode 100644 index 7902ac315b..a92b556f6a --- a/database/seeds/DatabaseSeeder.php +++ b/database/seeds/DatabaseSeeder.php @@ -28,7 +28,6 @@ class DatabaseSeeder extends Seeder $this->call(TransactionCurrencySeeder::class); $this->call(TransactionTypeSeeder::class); $this->call(PermissionSeeder::class); - $this->call(TestDataSeeder::class); } } diff --git a/database/seeds/PermissionSeeder.php b/database/seeds/PermissionSeeder.php index 1bf36c21e7..2916470354 100644 --- a/database/seeds/PermissionSeeder.php +++ b/database/seeds/PermissionSeeder.php @@ -29,6 +29,12 @@ class PermissionSeeder extends Seeder $owner->description = 'User runs this instance of FF3'; // optional $owner->save(); + $demo = new Role; + $demo->name ='demo'; + $demo->display_name = 'Demo User'; + $demo->description = 'User is a demo user'; + $demo->save(); + } } diff --git a/database/seeds/TestDataSeeder.php b/database/seeds/TestDataSeeder.php deleted file mode 100644 index 334b4725dd..0000000000 --- a/database/seeds/TestDataSeeder.php +++ /dev/null @@ -1,55 +0,0 @@ -exists($fileName)) { - Log::debug('Now seeding ' . $fileName); - $file = json_decode($disk->get($fileName), true); - - if (is_array($file)) { - // run the file: - TestData::run($file); - return; - } - Log::error('No valid data found (' . $fileName . ') for environment ' . $env); - return; - - } - Log::info('No seed file (' . $fileName . ') for environment ' . $env); - } -} diff --git a/database/seeds/TransactionCurrencySeeder.php b/database/seeds/TransactionCurrencySeeder.php index 887ce75f4a..f1204569a3 100644 --- a/database/seeds/TransactionCurrencySeeder.php +++ b/database/seeds/TransactionCurrencySeeder.php @@ -23,11 +23,13 @@ class TransactionCurrencySeeder extends Seeder { DB::table('transaction_currencies')->delete(); - TransactionCurrency::create(['code' => 'EUR', 'name' => 'Euro', 'symbol' => '€']); - TransactionCurrency::create(['code' => 'USD', 'name' => 'US Dollar', 'symbol' => '$']); - TransactionCurrency::create(['code' => 'HUF', 'name' => 'Hungarian forint', 'symbol' => 'Ft']); - TransactionCurrency::create(['code' => 'BRL', 'name' => 'Real', 'symbol' => 'R$']); - TransactionCurrency::create(['code' => 'GBP', 'name' => 'British Pound', 'symbol' => '£']); + TransactionCurrency::create(['code' => 'EUR', 'name' => 'Euro', 'symbol' => '€', 'decimal_places' => 2]); + TransactionCurrency::create(['code' => 'USD', 'name' => 'US Dollar', 'symbol' => '$', 'decimal_places' => 2]); + TransactionCurrency::create(['code' => 'HUF', 'name' => 'Hungarian forint', 'symbol' => 'Ft', 'decimal_places' => 2]); + TransactionCurrency::create(['code' => 'BRL', 'name' => 'Real', 'symbol' => 'R$', 'decimal_places' => 2]); + 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]); } } diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000000..c8ce8bea02 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,16 @@ +version: '2' + +services: + firefly-db: + volumes: + - firefly-dev-storage:/var/lib/mysql + firefly-app: + volumes: + - .:/var/www/firefly-iii + environment: + - INIT_DATABASE=yes + - FF_APP_ENV=development + +volumes: + firefly-dev-storage: + driver: local diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000000..68fdff69a1 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,12 @@ +version: '2' + +services: + firefly-app: + environment: + - FF_APP_ENV=production + volumes: + - firefly-app-storage:/var/www/firefly-iii/storage + +volumes: + firefly-app-storage: + driver: local diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..92f79e2ea6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: '2' + +services: + firefly-db: + image: mysql:8 + environment: + - MYSQL_DATABASE=firefly_db + - MYSQL_USER=firefly_db + - MYSQL_PASSWORD=firefly_db_secret + - MYSQL_RANDOM_ROOT_PASSWORD=yes + volumes: + - firefly-storage:/var/lib/mysql + + firefly-app: + image: firefly-iii + build: . + environment: + - FF_DB_HOST=firefly-db + - FF_DB_NAME=firefly_db + - FF_DB_USER=firefly_db + - FF_DB_PASSWORD=firefly_db_secret + - FF_APP_KEY=SomeRandomStringOf32CharsExactly + ports: + - "80:80" + links: + - firefly-db + +volumes: + firefly-storage: + driver: local diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000000..0baf22e862 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +cat .env.docker | envsubst > .env + +if [ "${INIT_DATABASE:="no"}" = "yes" ]; then + echo "Init database detected, checking mysql status" + # depends on your machine, but it may take a file to boot mysql container the first time + until php artisan firefly:verify &>/dev/null + do + echo "waiting mysql" + sleep 10 + done + php artisan migrate:refresh --seed +fi + +exec apache2-foreground diff --git a/phpunit.coverage.xml b/phpunit.coverage.xml new file mode 100755 index 0000000000..d643a5d6ec --- /dev/null +++ b/phpunit.coverage.xml @@ -0,0 +1,43 @@ + + + + + ./tests/Feature + + + + ./tests/Unit + + + + + + + + + + ./app + + + vendor/ + + + + + + + + + + + + diff --git a/phpunit.xml b/phpunit.xml index 712e0af587..b0e315533c 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,16 +7,24 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false"> + beStrictAboutOutputDuringTests="true" + stopOnFailure="true"> - - ./tests + + ./tests/Feature + + + + ./tests/Unit - + ./app + + vendor/ + diff --git a/public/android-chrome-144x144.png b/public/android-chrome-144x144.png deleted file mode 100644 index 0fbc935ef5..0000000000 Binary files a/public/android-chrome-144x144.png and /dev/null differ diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png index fb8549caef..e0c3f93dd2 100644 Binary files a/public/android-chrome-192x192.png and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-36x36.png b/public/android-chrome-36x36.png deleted file mode 100644 index c101427bd7..0000000000 Binary files a/public/android-chrome-36x36.png and /dev/null differ diff --git a/public/android-chrome-48x48.png b/public/android-chrome-48x48.png deleted file mode 100644 index 3817a33ab7..0000000000 Binary files a/public/android-chrome-48x48.png and /dev/null differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 0000000000..b7e4558446 Binary files /dev/null and b/public/android-chrome-512x512.png differ diff --git a/public/android-chrome-72x72.png b/public/android-chrome-72x72.png deleted file mode 100644 index 92ae9f437b..0000000000 Binary files a/public/android-chrome-72x72.png and /dev/null differ diff --git a/public/android-chrome-96x96.png b/public/android-chrome-96x96.png deleted file mode 100644 index 3602979589..0000000000 Binary files a/public/android-chrome-96x96.png and /dev/null differ diff --git a/public/android-chrome-manifest.json b/public/android-chrome-manifest.json deleted file mode 100644 index b07eb63253..0000000000 --- a/public/android-chrome-manifest.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "My app", - "icons": [ - { - "src": "\/android-chrome-36x36.png", - "sizes": "36x36", - "type": "image\/png", - "density": "0.75" - }, - { - "src": "\/android-chrome-48x48.png", - "sizes": "48x48", - "type": "image\/png", - "density": "1.0" - }, - { - "src": "\/android-chrome-72x72.png", - "sizes": "72x72", - "type": "image\/png", - "density": "1.5" - }, - { - "src": "\/android-chrome-96x96.png", - "sizes": "96x96", - "type": "image\/png", - "density": "2.0" - }, - { - "src": "\/android-chrome-144x144.png", - "sizes": "144x144", - "type": "image\/png", - "density": "3.0" - }, - { - "src": "\/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image\/png", - "density": "4.0" - } - ] -} diff --git a/public/apple-touch-icon-114x114.png b/public/apple-touch-icon-114x114.png deleted file mode 100644 index 498ea65bd1..0000000000 Binary files a/public/apple-touch-icon-114x114.png and /dev/null differ diff --git a/public/apple-touch-icon-120x120.png b/public/apple-touch-icon-120x120.png deleted file mode 100644 index 542d5330e4..0000000000 Binary files a/public/apple-touch-icon-120x120.png and /dev/null differ diff --git a/public/apple-touch-icon-144x144.png b/public/apple-touch-icon-144x144.png deleted file mode 100644 index d4aab1e910..0000000000 Binary files a/public/apple-touch-icon-144x144.png and /dev/null differ diff --git a/public/apple-touch-icon-152x152.png b/public/apple-touch-icon-152x152.png deleted file mode 100644 index 9e51ddfb64..0000000000 Binary files a/public/apple-touch-icon-152x152.png and /dev/null differ diff --git a/public/apple-touch-icon-180x180.png b/public/apple-touch-icon-180x180.png deleted file mode 100644 index c0f50f32f5..0000000000 Binary files a/public/apple-touch-icon-180x180.png and /dev/null differ diff --git a/public/apple-touch-icon-57x57.png b/public/apple-touch-icon-57x57.png deleted file mode 100644 index 414e32f2ee..0000000000 Binary files a/public/apple-touch-icon-57x57.png and /dev/null differ diff --git a/public/apple-touch-icon-60x60.png b/public/apple-touch-icon-60x60.png deleted file mode 100644 index e05470b64b..0000000000 Binary files a/public/apple-touch-icon-60x60.png and /dev/null differ diff --git a/public/apple-touch-icon-72x72.png b/public/apple-touch-icon-72x72.png deleted file mode 100644 index d2080d08e2..0000000000 Binary files a/public/apple-touch-icon-72x72.png and /dev/null differ diff --git a/public/apple-touch-icon-76x76.png b/public/apple-touch-icon-76x76.png deleted file mode 100644 index 47bd65a56d..0000000000 Binary files a/public/apple-touch-icon-76x76.png and /dev/null differ diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png deleted file mode 100644 index 0b6bb71456..0000000000 Binary files a/public/apple-touch-icon-precomposed.png and /dev/null differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index c0f50f32f5..6d2d88d80c 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/browserconfig.xml b/public/browserconfig.xml index 6d6066d3f2..74bb89ac30 100644 --- a/public/browserconfig.xml +++ b/public/browserconfig.xml @@ -2,10 +2,7 @@ - - - - + #da532c diff --git a/public/css/bootstrap-multiselect.css b/public/css/bootstrap-multiselect.css new file mode 100755 index 0000000000..5acaf9f7ab --- /dev/null +++ b/public/css/bootstrap-multiselect.css @@ -0,0 +1 @@ +span.multiselect-native-select{position:relative}span.multiselect-native-select select{border:0!important;clip:rect(0 0 0 0)!important;height:1px!important;margin:-1px -1px -1px -3px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;left:50%;top:30px}.multiselect-container{position:absolute;list-style-type:none;margin:0;padding:0}.multiselect-container .input-group{margin:5px}.multiselect-container>li{padding:0}.multiselect-container>li>a.multiselect-all label{font-weight:700}.multiselect-container>li.multiselect-group label{margin:0;padding:3px 20px 3px 20px;height:100%;font-weight:700}.multiselect-container>li.multiselect-group-clickable label{cursor:pointer}.multiselect-container>li>a{padding:0}.multiselect-container>li>a>label{margin:0;height:100%;cursor:pointer;font-weight:400;padding:3px 20px 3px 40px}.multiselect-container>li>a>label.radio,.multiselect-container>li>a>label.checkbox{margin:0}.multiselect-container>li>a>label>input[type=checkbox]{margin-bottom:5px}.btn-group>.btn-group:nth-child(2)>.multiselect.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.form-inline .multiselect-container label.checkbox,.form-inline .multiselect-container label.radio{padding:3px 20px 3px 40px}.form-inline .multiselect-container li a label.checkbox input[type=checkbox],.form-inline .multiselect-container li a label.radio input[type=radio]{margin-left:-20px;margin-right:0} diff --git a/public/css/firefly.css b/public/css/firefly.css index bd46d1e920..cc051e6442 100644 --- a/public/css/firefly.css +++ b/public/css/firefly.css @@ -1,3 +1,13 @@ +/* + * firefly.css + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + #daterange { cursor: pointer; } @@ -21,7 +31,7 @@ body.waiting * { } .preferences-box { - border:1px #ddd solid; + border: 1px #ddd solid; border-radius: 4px 4px 0 0; padding: 15px; margin: 15px; @@ -85,4 +95,19 @@ body.waiting * { .loading { background: url('/images/loading-small.gif') no-repeat center center; min-height: 30px; -} \ No newline at end of file +} + +@media print { + a[href]:after { + content: none !important; + } +} + +.edit_tr_buttons { + white-space: nowrap; +} + +.edit_tr_buttons .btn { + float: none; + display: inline-block; +} diff --git a/public/css/jquery-ui/images/ui-icons_444444_256x240.png b/public/css/jquery-ui/images/ui-icons_444444_256x240.png new file mode 100644 index 0000000000..19f664d970 Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_444444_256x240.png differ diff --git a/public/css/jquery-ui/images/ui-icons_555555_256x240.png b/public/css/jquery-ui/images/ui-icons_555555_256x240.png new file mode 100644 index 0000000000..e965f6d97c Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_555555_256x240.png differ diff --git a/public/css/jquery-ui/images/ui-icons_777620_256x240.png b/public/css/jquery-ui/images/ui-icons_777620_256x240.png new file mode 100644 index 0000000000..9785948a29 Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_777620_256x240.png differ diff --git a/public/css/jquery-ui/images/ui-icons_777777_256x240.png b/public/css/jquery-ui/images/ui-icons_777777_256x240.png new file mode 100644 index 0000000000..323c4564a7 Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_777777_256x240.png differ diff --git a/public/css/jquery-ui/images/ui-icons_cc0000_256x240.png b/public/css/jquery-ui/images/ui-icons_cc0000_256x240.png new file mode 100644 index 0000000000..45ac7787cd Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_cc0000_256x240.png differ diff --git a/public/css/jquery-ui/images/ui-icons_ffffff_256x240.png b/public/css/jquery-ui/images/ui-icons_ffffff_256x240.png new file mode 100644 index 0000000000..fe41d2d0fd Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_ffffff_256x240.png differ diff --git a/public/css/jquery-ui/jquery-ui.structure.min.css b/public/css/jquery-ui/jquery-ui.structure.min.css new file mode 100644 index 0000000000..882d445bde --- /dev/null +++ b/public/css/jquery-ui/jquery-ui.structure.min.css @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.12.1 - 2017-01-15 +* http://jqueryui.com +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em} \ No newline at end of file diff --git a/public/css/jquery-ui/jquery-ui.theme.min.css b/public/css/jquery-ui/jquery-ui.theme.min.css new file mode 100644 index 0000000000..4c88d3c36b --- /dev/null +++ b/public/css/jquery-ui/jquery-ui.theme.min.css @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.12.1 - 2017-01-15 +* http://jqueryui.com +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666} \ No newline at end of file diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png index 496df9f39a..a53c31ccf4 100644 Binary files a/public/favicon-16x16.png and b/public/favicon-16x16.png differ diff --git a/public/favicon-194x194.png b/public/favicon-194x194.png deleted file mode 100644 index 25d5884833..0000000000 Binary files a/public/favicon-194x194.png and /dev/null differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png index c729437261..da1a8b0f37 100644 Binary files a/public/favicon-32x32.png and b/public/favicon-32x32.png differ diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png deleted file mode 100644 index 555cb24669..0000000000 Binary files a/public/favicon-96x96.png and /dev/null differ diff --git a/public/favicon.ico b/public/favicon.ico index 78c3c142f3..c111de1c1e 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/fonts/lato-100.woff2 b/public/fonts/lato-100.woff2 new file mode 100644 index 0000000000..c9da634eac Binary files /dev/null and b/public/fonts/lato-100.woff2 differ diff --git a/public/fonts/roboto-light-300.woff2 b/public/fonts/roboto-light-300.woff2 new file mode 100644 index 0000000000..4411cbc875 Binary files /dev/null and b/public/fonts/roboto-light-300.woff2 differ diff --git a/public/index.php b/public/index.php index 4415ad6b6c..7f21d36fb5 100644 --- a/public/index.php +++ b/public/index.php @@ -3,19 +3,14 @@ * index.php * Copyright (C) 2016 thegrumpydictator@gmail.com * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * 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. */ declare(strict_types = 1); -/** - * Laravel - A PHP Framework For Web Artisans - * - * @package Laravel - * @author Taylor Otwell - */ - /* |-------------------------------------------------------------------------- | Register The Auto Loader diff --git a/public/js/ff/accounts/create.js b/public/js/ff/accounts/create.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/accounts/create.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/accounts/edit.js b/public/js/ff/accounts/edit.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/accounts/edit.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/accounts/show.js b/public/js/ff/accounts/show.js index f913802ab3..b3b1093c7a 100644 --- a/public/js/ff/accounts/show.js +++ b/public/js/ff/accounts/show.js @@ -1,7 +1,15 @@ -/* global $, lineChart, accountID, token */ +/* + * show.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ +/** global: chartUri, incomeCategoryUri, expenseCategoryUri, expenseBudgetUri */ -// Return a helper with preserved width of cells var fixHelper = function (e, tr) { "use strict"; var $originals = tr.children(); @@ -15,10 +23,11 @@ var fixHelper = function (e, tr) { $(function () { "use strict"; - if (typeof(lineChart) === "function" && typeof accountID !== 'undefined') { + lineChart(chartUri, 'overview-chart'); + pieChart(incomeCategoryUri, 'account-cat-in'); + pieChart(expenseCategoryUri, 'account-cat-out'); + pieChart(expenseBudgetUri, 'account-budget-out'); - lineChart('chart/account/' + accountID, 'overview-chart'); - } // sortable! if (typeof $(".sortable-table tbody").sortable !== "undefined") { @@ -45,9 +54,7 @@ $(function () { ui.placeholder.html(' '); } } - ).disableSelection(); - } else { - console.log('its null'); + ); } }); @@ -55,7 +62,6 @@ $(function () { function sortStop(event, ui) { "use strict"; var current = $(ui.item); - console.log('sort stop'); var thisDate = current.data('date'); var originalBG = current.css('backgroundColor'); @@ -64,6 +70,7 @@ function sortStop(event, ui) { // animate something with color: current.animate({backgroundColor: "#d9534f"}, 200, function () { $(this).animate({backgroundColor: originalBG}, 200); + return undefined; }); return false; @@ -79,9 +86,11 @@ function sortStop(event, ui) { }); // do extra animation when done? - $.post('transaction/reorder', {items: submit, date: thisDate, _token: token}); + $.post('transactions/reorder', {items: submit, date: thisDate}); current.animate({backgroundColor: "#5cb85c"}, 200, function () { $(this).animate({backgroundColor: originalBG}, 200); + return undefined; }); + return undefined; } diff --git a/public/js/ff/accounts/show_with_date.js b/public/js/ff/accounts/show_with_date.js deleted file mode 100644 index 8ad1376312..0000000000 --- a/public/js/ff/accounts/show_with_date.js +++ /dev/null @@ -1,98 +0,0 @@ -/* - * show_with_date.js - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -/* global $, lineChart, dateString, accountID, token */ - - -// Return a helper with preserved width of cells -var fixHelper = function (e, tr) { - "use strict"; - var $originals = tr.children(); - var $helper = tr.clone(); - $helper.children().each(function (index) { - // Set helper cell sizes to match the original sizes - $(this).width($originals.eq(index).width()); - }); - return $helper; -}; - -$(function () { - "use strict"; - if (typeof(lineChart) === "function" && - typeof accountID !== 'undefined' && - typeof dateString !== 'undefined' - ) { - - lineChart('chart/account/' + accountID + '/' + dateString, 'period-specific-account'); - } - - // sortable! - if (typeof $(".sortable-table tbody").sortable !== "undefined") { - $(".sortable-table tbody").sortable( - { - helper: fixHelper, - items: 'tr:not(.ignore)', - stop: sortStop, - handle: '.handle', - start: function (event, ui) { - // Build a placeholder cell that spans all the cells in the row - var cellCount = 0; - $('td, th', ui.helper).each(function () { - // For each TD or TH try and get it's colspan attribute, and add that or 1 to the total - var colspan = 1; - var colspanAttr = $(this).attr('colspan'); - if (colspanAttr > 1) { - colspan = colspanAttr; - } - cellCount += colspan; - }); - - // Add the placeholder UI - note that this is the item's content, so TD rather than TR - ui.placeholder.html(' '); - } - } - ).disableSelection(); - } else { - console.log('its null'); - } - -}); - -function sortStop(event, ui) { - "use strict"; - var current = $(ui.item); - console.log('sort stop'); - var thisDate = current.data('date'); - var originalBG = current.css('backgroundColor'); - - - if (current.prev().data('date') !== thisDate && current.next().data('date') !== thisDate) { - // animate something with color: - current.animate({backgroundColor: "#d9534f"}, 200, function () { - $(this).animate({backgroundColor: originalBG}, 200); - }); - - return false; - } - - // do update - var list = $('tr[data-date="' + thisDate + '"]'); - var submit = []; - $.each(list, function (i, v) { - var row = $(v); - var id = row.data('id'); - submit.push(id); - }); - - // do extra animation when done? - $.post('transaction/reorder', {items: submit, date: thisDate, _token: token}); - - current.animate({backgroundColor: "#5cb85c"}, 200, function () { - $(this).animate({backgroundColor: originalBG}, 200); - }); -} diff --git a/public/js/ff/bills/create.js b/public/js/ff/bills/create.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/bills/create.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/bills/edit.js b/public/js/ff/bills/edit.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/bills/edit.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/bills/show.js b/public/js/ff/bills/show.js index d383023ead..f67c246ca2 100644 --- a/public/js/ff/bills/show.js +++ b/public/js/ff/bills/show.js @@ -1,9 +1,17 @@ -/* global comboChart, billID */ +/* + * show.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: billUri */ $(function () { "use strict"; - if (typeof(columnChart) === 'function' && typeof(billID) !== 'undefined') { - columnChart('chart/bill/' + billID, 'bill-overview'); - } + columnChart(billUri, 'bill-overview'); } ); \ No newline at end of file diff --git a/public/js/ff/budgets/index.js b/public/js/ff/budgets/index.js index c8ca145734..936ae07b7f 100644 --- a/public/js/ff/budgets/index.js +++ b/public/js/ff/budgets/index.js @@ -1,4 +1,14 @@ -/* globals $, budgeted:true, currencySymbol, budgetIncomeTotal, columnChart, budgetedMuch, budgetedPercentage, token, budgetID, repetitionID, spent, lineChart */ +/* + * index.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: spent, budgeted, available, currencySymbol */ function drawSpentBar() { "use strict"; @@ -23,19 +33,19 @@ function drawBudgetedBar() { "use strict"; if ($('.budgetedBar').length > 0) { - var budgetedMuch = budgeted > budgetIncomeTotal; + var budgetedMuch = budgeted > available; // recalculate percentage: var pct; if (budgetedMuch) { // budgeted too much. - pct = (budgetIncomeTotal / budgeted) * 100; + pct = (available / budgeted) * 100; $('.budgetedBar .progress-bar-warning').css('width', pct + '%'); $('.budgetedBar .progress-bar-danger').css('width', (100 - pct) + '%'); $('.budgetedBar .progress-bar-info').css('width', 0); } else { - pct = (budgeted / budgetIncomeTotal) * 100; + pct = (budgeted / available) * 100; $('.budgetedBar .progress-bar-warning').css('width', 0); $('.budgetedBar .progress-bar-danger').css('width', 0); $('.budgetedBar .progress-bar-info').css('width', pct + '%'); @@ -62,7 +72,7 @@ function updateBudgetedAmounts(e) { drawBudgetedBar(); // send a post to Firefly to update the amount: - $.post('budgets/amount/' + id, {amount: value, _token: token}).done(function (data) { + $.post('budgets/amount/' + id, {amount: value}).done(function (data) { // update the link if relevant: if (data.repetition > 0) { $('.budget-link[data-id="' + id + '"]').attr('href', 'budgets/show/' + id + '/' + data.repetition); @@ -71,11 +81,6 @@ function updateBudgetedAmounts(e) { } }); } - - - console.log('Budget id is ' + id); - console.log('Difference = ' + (value - original )); - } $(function () { @@ -94,17 +99,6 @@ $(function () { */ $('input[type="number"]').on('input', updateBudgetedAmounts); - - /* - Draw the charts, if necessary: - */ - if (typeof budgetID !== 'undefined' && typeof repetitionID === 'undefined') { - columnChart('chart/budget/' + budgetID, 'budgetOverview'); - } - if (typeof budgetID !== 'undefined' && typeof repetitionID !== 'undefined') { - lineChart('chart/budget/' + budgetID + '/' + repetitionID, 'budgetOverview'); - } - }); function updateIncome() { diff --git a/public/js/ff/budgets/show.js b/public/js/ff/budgets/show.js index c8ca145734..07cf18ed9e 100644 --- a/public/js/ff/budgets/show.js +++ b/public/js/ff/budgets/show.js @@ -1,117 +1,18 @@ -/* globals $, budgeted:true, currencySymbol, budgetIncomeTotal, columnChart, budgetedMuch, budgetedPercentage, token, budgetID, repetitionID, spent, lineChart */ +/* + * show.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ -function drawSpentBar() { - "use strict"; - if ($('.spentBar').length > 0) { - var overspent = spent > budgeted; - var pct; - - if (overspent) { - // draw overspent bar - pct = (budgeted / spent) * 100; - $('.spentBar .progress-bar-warning').css('width', pct + '%'); - $('.spentBar .progress-bar-danger').css('width', (100 - pct) + '%'); - } else { - // draw normal bar: - pct = (spent / budgeted) * 100; - $('.spentBar .progress-bar-info').css('width', pct + '%'); - } - } -} - -function drawBudgetedBar() { - "use strict"; - - if ($('.budgetedBar').length > 0) { - var budgetedMuch = budgeted > budgetIncomeTotal; - - // recalculate percentage: - - var pct; - if (budgetedMuch) { - // budgeted too much. - pct = (budgetIncomeTotal / budgeted) * 100; - $('.budgetedBar .progress-bar-warning').css('width', pct + '%'); - $('.budgetedBar .progress-bar-danger').css('width', (100 - pct) + '%'); - $('.budgetedBar .progress-bar-info').css('width', 0); - } else { - pct = (budgeted / budgetIncomeTotal) * 100; - $('.budgetedBar .progress-bar-warning').css('width', 0); - $('.budgetedBar .progress-bar-danger').css('width', 0); - $('.budgetedBar .progress-bar-info').css('width', pct + '%'); - } - - $('#budgetedAmount').html(currencySymbol + ' ' + budgeted.toFixed(2)); - } -} - -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; - if (difference !== 0) { - // add difference to 'budgeted' var - budgeted = budgeted + difference; - - // update original: - target.data('original', value); - // run drawBudgetedBar() again: - drawBudgetedBar(); - - // send a post to Firefly to update the amount: - $.post('budgets/amount/' + id, {amount: value, _token: token}).done(function (data) { - // update the link if relevant: - if (data.repetition > 0) { - $('.budget-link[data-id="' + id + '"]').attr('href', 'budgets/show/' + id + '/' + data.repetition); - } else { - $('.budget-link[data-id="' + id + '"]').attr('href', 'budgets/show/' + id); - } - }); - } - - - console.log('Budget id is ' + id); - console.log('Difference = ' + (value - original )); - -} +/** global: budgetChartUri */ $(function () { "use strict"; - $('.updateIncome').on('click', updateIncome); - - /* - On start, fill the "spent"-bar using the content from the page. - */ - drawSpentBar(); - drawBudgetedBar(); - - /* - When the input changes, update the percentages for the budgeted bar: - */ - $('input[type="number"]').on('input', updateBudgetedAmounts); - - - /* - Draw the charts, if necessary: - */ - if (typeof budgetID !== 'undefined' && typeof repetitionID === 'undefined') { - columnChart('chart/budget/' + budgetID, 'budgetOverview'); - } - if (typeof budgetID !== 'undefined' && typeof repetitionID !== 'undefined') { - lineChart('chart/budget/' + budgetID + '/' + repetitionID, 'budgetOverview'); - } + columnChart(budgetChartUri, 'budgetOverview'); }); - -function updateIncome() { - "use strict"; - $('#defaultModal').empty().load('budgets/income', function () { - $('#defaultModal').modal('show'); - }); - - return false; -} diff --git a/public/js/ff/categories/index.js b/public/js/ff/categories/index.js index 8bf673924b..ca8ef07e42 100644 --- a/public/js/ff/categories/index.js +++ b/public/js/ff/categories/index.js @@ -1,19 +1,13 @@ -/* globals $, categoryID, columnChart, categoryDate */ +/* + * index.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + $(function () { "use strict"; - if (typeof categoryID !== 'undefined') { - // more splits: - if ($('#all').length > 0) { - columnChart('chart/category/' + categoryID + '/all', 'all'); - } - if ($('#period').length > 0) { - columnChart('chart/category/' + categoryID + '/period', 'period'); - } - - } - if (typeof categoryID !== 'undefined' && typeof categoryDate !== undefined) { - columnChart('chart/category/' + categoryID + '/period/' + categoryDate, 'period-specific-period'); - } - - }); \ No newline at end of file diff --git a/public/js/ff/categories/show-by-date.js b/public/js/ff/categories/show-by-date.js new file mode 100644 index 0000000000..e97dab4db8 --- /dev/null +++ b/public/js/ff/categories/show-by-date.js @@ -0,0 +1,16 @@ +/* + * show-by-date.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: specific */ + +$(function () { + "use strict"; + columnChart(specific, 'period-specific-period'); +}); \ No newline at end of file diff --git a/public/js/ff/categories/show.js b/public/js/ff/categories/show.js index 8bf673924b..e1d5905d24 100644 --- a/public/js/ff/categories/show.js +++ b/public/js/ff/categories/show.js @@ -1,19 +1,18 @@ -/* globals $, categoryID, columnChart, categoryDate */ +/* + * show.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: all, current, specific */ + $(function () { "use strict"; - if (typeof categoryID !== 'undefined') { - // more splits: - if ($('#all').length > 0) { - columnChart('chart/category/' + categoryID + '/all', 'all'); - } - if ($('#period').length > 0) { - columnChart('chart/category/' + categoryID + '/period', 'period'); - } - - } - if (typeof categoryID !== 'undefined' && typeof categoryDate !== undefined) { - columnChart('chart/category/' + categoryID + '/period/' + categoryDate, 'period-specific-period'); - } - - + columnChart(all, 'all'); + columnChart(current, 'period'); + columnChart(specific, 'period-specific-period'); }); \ No newline at end of file diff --git a/public/js/ff/categories/show_with_date.js b/public/js/ff/categories/show_with_date.js deleted file mode 100644 index 8bf673924b..0000000000 --- a/public/js/ff/categories/show_with_date.js +++ /dev/null @@ -1,19 +0,0 @@ -/* globals $, categoryID, columnChart, categoryDate */ -$(function () { - "use strict"; - if (typeof categoryID !== 'undefined') { - // more splits: - if ($('#all').length > 0) { - columnChart('chart/category/' + categoryID + '/all', 'all'); - } - if ($('#period').length > 0) { - columnChart('chart/category/' + categoryID + '/period', 'period'); - } - - } - if (typeof categoryID !== 'undefined' && typeof categoryDate !== undefined) { - columnChart('chart/category/' + categoryID + '/period/' + categoryDate, 'period-specific-period'); - } - - -}); \ No newline at end of file diff --git a/public/js/ff/charts.defaults.js b/public/js/ff/charts.defaults.js new file mode 100644 index 0000000000..4679f6565b --- /dev/null +++ b/public/js/ff/charts.defaults.js @@ -0,0 +1,63 @@ +/* + * charts.defaults.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: accounting */ + +var defaultChartOptions = { + elements: { + line: { + cubicInterpolationMode: 'monotone' + } + }, + scales: { + xAxes: [ + { + gridLines: { + display: false + } + } + ], + yAxes: [{ + display: true, + ticks: { + callback: function (tickValue) { + "use strict"; + return accounting.formatMoney(tickValue); + + }, + beginAtZero: true + } + + }] + }, + tooltips: { + mode: 'label', + callbacks: { + label: function (tooltipItem, data) { + "use strict"; + return data.datasets[tooltipItem.datasetIndex].label + ': ' + accounting.formatMoney(tooltipItem.yLabel); + } + } + } +}; + +var defaultPieOptions = { + tooltips: { + callbacks: { + label: function (tooltipItem, data) { + "use strict"; + var value = data.datasets[0].data[tooltipItem.index]; + return data.labels[tooltipItem.index] + ': ' + accounting.formatMoney(value); + } + } + }, + maintainAspectRatio: true, + responsive: true +}; \ No newline at end of file diff --git a/public/js/ff/charts.js b/public/js/ff/charts.js index 7201e1f0cb..d402ab67f8 100644 --- a/public/js/ff/charts.js +++ b/public/js/ff/charts.js @@ -2,11 +2,13 @@ * charts.js * Copyright (C) 2016 thegrumpydictator@gmail.com * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * 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. */ - -/* globals $, Chart, currencySymbol,mon_decimal_point ,accounting, mon_thousands_sep, frac_digits */ +/** global: Chart, defaultChartOptions, accounting, defaultPieOptions, noDataForChart */ +var allCharts = {}; /* Make some colours: @@ -29,7 +31,6 @@ var colourSet = [ [240, 98, 146], [0, 121, 107], [194, 24, 91] - ]; var fillColors = []; @@ -46,332 +47,240 @@ Chart.defaults.global.animation.duration = 0; Chart.defaults.global.responsive = true; Chart.defaults.global.maintainAspectRatio = false; -/* - Set default options: + +/** + * + * @param data + * @returns {{}} */ -var defaultAreaOptions = { - scales: { - xAxes: [ - { - gridLines: { - display: false - } - } - ], - yAxes: [{ - display: true, - ticks: { - callback: function (tickValue, index, ticks) { - "use strict"; - return accounting.formatMoney(tickValue); +function colorizeData(data) { + var newData = {}; + newData.datasets = []; - } - } - }] - }, - tooltips: { - mode: 'label', - callbacks: { - label: function (tooltipItem, data) { - "use strict"; - return data.datasets[tooltipItem.datasetIndex].label + ': ' + accounting.formatMoney(tooltipItem.yLabel); - } - } + for (var i = 0; i < data.count; i++) { + newData.labels = data.labels; + var dataset = data.datasets[i]; + dataset.backgroundColor = fillColors[i]; + newData.datasets.push(dataset); } -}; - - -var defaultPieOptions = { - tooltips: { - callbacks: { - label: function (tooltipItem, data) { - "use strict"; - var value = data.datasets[0].data[tooltipItem.index]; - return data.labels[tooltipItem.index] + ': ' + accounting.formatMoney(value); - } - } - } -}; - - -var defaultLineOptions = { - scales: { - xAxes: [ - { - gridLines: { - display: false - } - } - ], - yAxes: [{ - display: true, - ticks: { - callback: function (tickValue, index, ticks) { - "use strict"; - return accounting.formatMoney(tickValue); - - } - } - }] - }, - tooltips: { - mode: 'label', - callbacks: { - label: function (tooltipItem, data) { - "use strict"; - return data.datasets[tooltipItem.datasetIndex].label + ': ' + accounting.formatMoney(tooltipItem.yLabel); - } - } - } -}; - -var defaultColumnOptions = { - scales: { - xAxes: [ - { - gridLines: { - display: false - } - } - ], - yAxes: [{ - ticks: { - callback: function (tickValue, index, ticks) { - "use strict"; - return accounting.formatMoney(tickValue); - }, - beginAtZero: true - } - }] - }, - elements: { - line: { - fill: false - } - }, - tooltips: { - mode: 'label', - callbacks: { - label: function (tooltipItem, data) { - "use strict"; - return data.datasets[tooltipItem.datasetIndex].label + ': ' + accounting.formatMoney(tooltipItem.yLabel); - } - } - } -}; - -var defaultStackedColumnOptions = { - stacked: true, - scales: { - xAxes: [{ - stacked: true, - gridLines: { - display: false - } - }], - yAxes: [{ - stacked: true, - ticks: { - callback: function (tickValue, index, ticks) { - "use strict"; - return accounting.formatMoney(tickValue); - - } - } - }] - }, - tooltips: { - mode: 'label', - callbacks: { - label: function (tooltipItem, data) { - "use strict"; - return data.datasets[tooltipItem.datasetIndex].label + ': ' + accounting.formatMoney(tooltipItem.yLabel); - } - } - } -}; + return newData; +} /** * Function to draw a line chart: - * @param URL + * @param URI * @param container - * @param options */ -function lineChart(URL, container, options) { +function lineChart(URI, container) { "use strict"; - $.getJSON(URL).done(function (data) { - var ctx = document.getElementById(container).getContext("2d"); - var newData = {}; - newData.datasets = []; + var colorData = true; + var options = defaultChartOptions; + var chartType = 'line'; - for (var i = 0; i < data.count; i++) { - newData.labels = data.labels; - var dataset = data.datasets[i]; - dataset.backgroundColor = fillColors[i]; - newData.datasets.push(dataset); - } - - new Chart(ctx, { - type: 'line', - data: data, - options: defaultLineOptions - }); - - }).fail(function () { - $('#' + container).addClass('general-chart-error'); - }); - console.log('URL for line chart : ' + URL); + drawAChart(URI, container, chartType, options, colorData); } /** - * Function to draw an area chart: + * Function to draw a chart with double Y Axes and stacked columns. * - * @param URL + * @param URI * @param container - * @param options */ -function areaChart(URL, container, options) { +function doubleYChart(URI, container) { "use strict"; - $.getJSON(URL).done(function (data) { - var ctx = document.getElementById(container).getContext("2d"); - var newData = {}; - newData.datasets = []; + var colorData = true; + var options = defaultChartOptions; + options.scales.yAxes = [ + // y axis 0: + { + display: true, + ticks: { + callback: function (tickValue) { + "use strict"; + return accounting.formatMoney(tickValue); - for (var i = 0; i < data.count; i++) { - newData.labels = data.labels; - var dataset = data.datasets[i]; - dataset.backgroundColor = fillColors[i]; - newData.datasets.push(dataset); + }, + beginAtZero: true + }, + position: "left", + "id": "y-axis-0" + }, + // and y axis 1: + { + display: true, + ticks: { + callback: function (tickValue) { + "use strict"; + return accounting.formatMoney(tickValue); + + }, + beginAtZero: true + }, + position: "right", + "id": "y-axis-1" } - new Chart(ctx, { - type: 'line', - data: newData, - options: defaultAreaOptions - }); + ]; + options.stacked = true; + options.scales.xAxes[0].stacked = true; - }).fail(function () { - $('#' + container).addClass('general-chart-error'); - }); + var chartType = 'bar'; + + drawAChart(URI, container, chartType, options, colorData); +} + +/** + * Function to draw a chart with double Y Axes and non stacked columns. + * + * @param URI + * @param container + */ +function doubleYNonStackedChart(URI, container) { + "use strict"; + + var colorData = true; + var options = defaultChartOptions; + options.scales.yAxes = [ + // y axis 0: + { + display: true, + ticks: { + callback: function (tickValue) { + "use strict"; + return accounting.formatMoney(tickValue); + + }, + beginAtZero: true + }, + position: "left", + "id": "y-axis-0" + }, + // and y axis 1: + { + display: true, + ticks: { + callback: function (tickValue) { + "use strict"; + return accounting.formatMoney(tickValue); + + }, + beginAtZero: true + }, + position: "right", + "id": "y-axis-1" + } + + ]; + var chartType = 'bar'; + + drawAChart(URI, container, chartType, options, colorData); +} + + +/** + * + * @param URI + * @param container + */ +function columnChart(URI, container) { + "use strict"; + + var colorData = true; + var options = defaultChartOptions; + var chartType = 'bar'; + + drawAChart(URI, container, chartType, options, colorData); - console.log('URL for area chart: ' + URL); } /** * - * @param URL + * @param URI * @param container - * @param options */ -function columnChart(URL, container, options) { +function stackedColumnChart(URI, container) { "use strict"; - options = options || {}; + var colorData = true; + var options = defaultChartOptions; + options.stacked = true; + options.scales.xAxes[0].stacked = true; - $.getJSON(URL).done(function (data) { + var chartType = 'bar'; - var result = true; - if (options.beforeDraw) { - result = options.beforeDraw(data, {url: URL, container: container}); - } - if (result === false) { - return; - } - console.log('Will draw columnChart(' + URL + ')'); - - var ctx = document.getElementById(container).getContext("2d"); - var newData = {}; - newData.datasets = []; - - for (var i = 0; i < data.count; i++) { - newData.labels = data.labels; - var dataset = data.datasets[i]; - dataset.backgroundColor = fillColors[i]; - newData.datasets.push(dataset); - } - new Chart(ctx, { - type: 'bar', - data: data, - options: defaultColumnOptions - }); - - }).fail(function () { - $('#' + container).addClass('general-chart-error'); - }); - console.log('URL for column chart : ' + URL); + drawAChart(URI, container, chartType, options, colorData); } /** * - * @param URL + * @param URI * @param container - * @param options */ -function stackedColumnChart(URL, container, options) { +function pieChart(URI, container) { "use strict"; - options = options || {}; + var colorData = false; + var options = defaultPieOptions; + var chartType = 'pie'; + + drawAChart(URI, container, chartType, options, colorData); + +} - $.getJSON(URL).done(function (data) { +/** + * @param URI + * @param container + * @param chartType + * @param options + * @param colorData + */ +function drawAChart(URI, container, chartType, options, colorData) { + if ($('#' + container).length === 0) { + return; + } - var result = true; - if (options.beforeDraw) { - result = options.beforeDraw(data, {url: URL, container: container}); - } - if (result === false) { + + $.getJSON(URI).done(function (data) { + $('#' + container).removeClass('general-chart-error'); + if (data.labels.length === 0) { + // remove the chart container + parent + var holder = $('#' + container).parent().parent(); + if (holder.hasClass('box') || holder.hasClass('box-body')) { + // find box-body: + var boxBody; + if (!holder.hasClass('box-body')) { + boxBody = holder.find('.box-body'); + } else { + boxBody = holder; + } + boxBody.empty().append($('

').append($('').text(noDataForChart))); + } return; } - var ctx = document.getElementById(container).getContext("2d"); - var newData = {}; - newData.datasets = []; - - for (var i = 0; i < data.count; i++) { - newData.labels = data.labels; - var dataset = data.datasets[i]; - dataset.backgroundColor = fillColors[i]; - newData.datasets.push(dataset); + if (colorData) { + data = colorizeData(data); } - new Chart(ctx, { - type: 'bar', - data: data, - options: defaultStackedColumnOptions - }); + if (allCharts.hasOwnProperty(container)) { + allCharts[container].data.datasets = data.datasets; + allCharts[container].data.labels = data.labels; + allCharts[container].update(); + } else { + // new chart! + var ctx = document.getElementById(container).getContext("2d"); + allCharts[container] = new Chart(ctx, { + type: chartType, + data: data, + options: options + }); + } }).fail(function () { $('#' + container).addClass('general-chart-error'); }); - console.log('URL for stacked column chart : ' + URL); -} - -/** - * - * @param URL - * @param container - * @param options - */ -function pieChart(URL, container, options) { - "use strict"; - - $.getJSON(URL).done(function (data) { - - var ctx = document.getElementById(container).getContext("2d"); - new Chart(ctx, { - type: 'pie', - data: data, - options: defaultPieOptions - }); - - }).fail(function () { - $('#' + container).addClass('general-chart-error'); - }); - - - console.log('URL for pie chart : ' + URL); - } diff --git a/public/js/ff/export/index.js b/public/js/ff/export/index.js index f4ae67a6b4..e7af126f51 100644 --- a/public/js/ff/export/index.js +++ b/public/js/ff/export/index.js @@ -1,13 +1,15 @@ -/* globals token, jobKey */ - /* * index.js * Copyright (C) 2016 thegrumpydictator@gmail.com * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * 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: jobKey, Modernizr */ + var intervalId = 0; $(function () { @@ -19,12 +21,19 @@ $(function () { // - return false, $('#export').submit(startExport); + + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } } ); function startExport() { "use strict"; - console.log('Start export...'); hideForm(); showLoading(); hideError(); @@ -75,15 +84,12 @@ function showError(text) { function callExport() { "use strict"; - console.log('Start callExport()...') var data = $('#export').serialize(); // call status, keep calling it until response is "finished"? intervalId = window.setInterval(checkStatus, 500); - $.post('export/submit', data).done(function (data) { - console.log('Export hath succeeded!'); - + $.post('export/submit', data).done(function () { // stop polling: window.clearTimeout(intervalId); @@ -114,7 +120,6 @@ function callExport() { function checkStatus() { "use strict"; - console.log('get status...'); $.getJSON('export/status/' + jobKey).done(function (data) { putStatusText(data.status); }); diff --git a/public/js/ff/firefly.js b/public/js/ff/firefly.js index d63eddbcc3..4f33376896 100644 --- a/public/js/ff/firefly.js +++ b/public/js/ff/firefly.js @@ -1,7 +1,24 @@ -/* globals token, dateRangeConfig, $, */ +/* + * firefly.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ +/** global: moment, accountingConfig, dateRangeConfig, accounting, currencySymbol, mon_decimal_point, frac_digits, showFullList, showOnlyTop, mon_thousands_sep */ + + $(function () { "use strict"; + $.ajaxSetup({ + headers: { + 'X-CSRF-Token': $('meta[name="_token"]').attr('content') + } + }); + // when you click on a currency, this happens: $('.currency-option').click(currencySelect); @@ -40,21 +57,19 @@ $(function () { $.post(dateRangeConfig.URL, { start: start.format('YYYY-MM-DD'), end: end.format('YYYY-MM-DD'), - label: label, - _token: token + label: label }).done(function () { - console.log('Succesfully sent new date range [' + start.format('YYYY-MM-DD') + '-' + end.format('YYYY-MM-DD') + '].'); window.location.reload(true); }).fail(function () { - console.log('Could not send new date range.'); alert('Could not change date range'); - }); - - //alert('A date range was chosen: ' + start.format('YYYY-MM-DD') + ' to ' + end.format('YYYY-MM-DD')); } ); + + // trigger list thing + listLengthInitial(); + }); function currencySelect(e) { @@ -97,7 +112,7 @@ function currencySelect(e) { accounting.settings = { currency: { symbol: currencySymbol, // default currency symbol is '$' - format: "%s %v", // controls output: %s = symbol, %v = value/number (can be object: see below) + format: accountingConfig, // controls output: %s = symbol, %v = value/number (can be object: see below) decimal: mon_decimal_point, // decimal point separator thousand: mon_thousands_sep, // thousands separator precision: frac_digits // decimal places @@ -108,3 +123,29 @@ accounting.settings = { decimal: "." } }; + + +function listLengthInitial() { + "use strict"; + $('.overListLength').hide(); + $('.listLengthTrigger').unbind('click').click(triggerList) +} + +function triggerList(e) { + "use strict"; + var link = $(e.target); + var table = link.parent().parent().parent().parent(); + if (table.attr('data-hidden') === 'no') { + // hide all elements, return false. + table.find('.overListLength').hide(); + table.attr('data-hidden', 'yes'); + link.text(showFullList); + return false; + } + // show all, return false + table.find('.overListLength').show(); + table.attr('data-hidden', 'no'); + link.text(showOnlyTop); + + return false; +} \ No newline at end of file diff --git a/public/js/ff/guest.js b/public/js/ff/guest.js index 7ba3e6bed9..886d5a1a51 100644 --- a/public/js/ff/guest.js +++ b/public/js/ff/guest.js @@ -1,3 +1,13 @@ +/* + * guest.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + $(function () { "use strict"; diff --git a/public/js/ff/help.js b/public/js/ff/help.js index 1d6e14e477..536a4c5c2e 100644 --- a/public/js/ff/help.js +++ b/public/js/ff/help.js @@ -1,3 +1,13 @@ +/* + * help.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + $(function () { "use strict"; $('#help').click(showHelp); diff --git a/public/js/ff/import/status.js b/public/js/ff/import/status.js index 2084858131..cf97ab9935 100644 --- a/public/js/ff/import/status.js +++ b/public/js/ff/import/status.js @@ -2,11 +2,13 @@ * status.js * Copyright (C) 2016 thegrumpydictator@gmail.com * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * 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. */ -/* globals $, jobImportUrl, jobStartUrl, token, langImportMultiError, langImportSingleError, langImportFatalError, langImportTimeOutError */ +/** global: jobImportUrl, langImportSingleError, langImportMultiError, jobStartUrl, langImportTimeOutError, langImportFinished, langImportFatalError */ var startedImport = false; var startInterval = 2000; @@ -28,20 +30,17 @@ $(function () { function checkImportStatus() { "use strict"; - console.log('checkImportStatus()'); $.getJSON(jobImportUrl).done(reportOnJobImport).fail(failedJobImport); } -function importComplete(data) { +function importComplete() { "use strict"; - console.log('importComplete()'); var bar = $('#import-status-bar'); bar.removeClass('active'); } function updateBar(data) { "use strict"; - console.log('updateBar()'); var bar = $('#import-status-bar'); if (data.showPercentage) { bar.addClass('progress-bar-success').removeClass('progress-bar-info'); @@ -50,7 +49,7 @@ function updateBar(data) { $('#import-status-bar').text(data.stepsDone + '/' + data.steps); if (data.percentage >= 100) { - importComplete(data); + importComplete(); return; } return; @@ -63,7 +62,6 @@ function updateBar(data) { function reportErrors(data) { "use strict"; - console.log('reportErrors()'); if (data.errors.length == 1) { $('#import-status-error-intro').text(langImportSingleError); //'An error has occured during the import. The import can continue, however.' @@ -83,21 +81,18 @@ function reportErrors(data) { function reportStatus(data) { "use strict"; - console.log('reportStatus()'); $('#import-status-txt').removeClass('text-danger').text(data.statusText); } function kickStartJob() { "use strict"; - console.log('kickStartJob()'); - $.post(jobStartUrl, {_token: token}); + $.post(jobStartUrl); startedTheImport(); startedImport = true; } function updateTimeout(data) { "use strict"; - console.log('updateTimeout()'); if (data.stepsDone != stepCount) { stepCount = data.stepsDone; currentLimit = 0; @@ -105,12 +100,10 @@ function updateTimeout(data) { } currentLimit = currentLimit + interval; - // console.log("stepCount: " + stepCount + ", stepsDone: " + data.stepsDone + ", currentLimit: " + currentLimit); } function timeoutError() { "use strict"; - console.log('timeoutError()'); // set status $('#import-status-txt').addClass('text-danger').text(langImportTimeOutError); @@ -121,13 +114,11 @@ function timeoutError() { function importJobFinished(data) { "use strict"; - console.log('importJobFinished() = ' + data.finished); return data.finished; } function finishedJob(data) { "use strict"; - console.log('finishedJob()'); // "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); @@ -142,7 +133,6 @@ function finishedJob(data) { function reportOnJobImport(data) { "use strict"; - console.log('reportOnJobImport()'); updateBar(data); reportErrors(data); reportStatus(data); @@ -173,13 +163,11 @@ function reportOnJobImport(data) { function startedTheImport() { "use strict"; - console.log('startedTheImport()'); setTimeout(checkImportStatus, interval); } function failedJobImport(jqxhr, textStatus, error) { "use strict"; - console.log('failedJobImport()'); // 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); diff --git a/public/js/ff/index.js b/public/js/ff/index.js index c714f83ac7..1cd119c383 100644 --- a/public/js/ff/index.js +++ b/public/js/ff/index.js @@ -1,10 +1,20 @@ -/* globals $, columnChart,showTour, Tour, google, lineChart, pieChart, stackedColumnChart, areaChart */ +/* + * index.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Tour, showTour, accountFrontpageUri, token, billCount, accountExpenseUri, accountRevenueUri */ $(function () { "use strict"; // do chart JS stuff. drawChart(); - if (showTour) { + if (showTour == true) { $.getJSON('json/tour').done(function (data) { var tour = new Tour( { @@ -16,8 +26,6 @@ $(function () { tour.init(); // Start the tour tour.start(); - }).fail(function () { - console.log('Already had tour.'); }); } @@ -32,44 +40,26 @@ function endTheTour() { function drawChart() { "use strict"; - areaChart('chart/account/frontpage', 'accounts-chart'); - pieChart('chart/bill/frontpage', 'bills-chart'); - stackedColumnChart('chart/budget/frontpage', 'budgets-chart', {beforeDraw: beforeDrawIsEmpty}); - columnChart('chart/category/frontpage', 'categories-chart', {beforeDraw: beforeDrawIsEmpty}); - columnChart('chart/account/expense', 'expense-accounts-chart', {beforeDraw: beforeDrawIsEmpty}); - columnChart('chart/account/revenue', 'revenue-accounts-chart', {beforeDraw: beforeDrawIsEmpty}); + lineChart(accountFrontpageUri, 'accounts-chart'); + if (billCount > 0) { + pieChart('chart/bill/frontpage', 'bills-chart'); + } + stackedColumnChart('chart/budget/frontpage', 'budgets-chart'); + columnChart('chart/category/frontpage', 'categories-chart'); + columnChart(accountExpenseUri, 'expense-accounts-chart'); + columnChart(accountRevenueUri, 'revenue-accounts-chart'); getBoxAmounts(); } -/** - * Removes a chart container if there is nothing for the chart to draw. - * - * @param data - * @param options - * @returns {boolean} - */ -function beforeDrawIsEmpty(data, options) { - "use strict"; - - // check if chart holds data. - if (data.labels.length === 0) { - // remove the chart container + parent - console.log(options.container + ' appears empty. Removed.'); - $('#' + options.container).parent().parent().remove(); - - // return false so script stops. - return false; - } - return true; -} - - function getBoxAmounts() { "use strict"; var boxes = ['in', 'out', 'bills-unpaid', 'bills-paid']; for (var x in boxes) { + if (!boxes.hasOwnProperty(x)) { + continue; + } var box = boxes[x]; $.getJSON('json/box/' + box).done(putData).fail(failData); } @@ -82,5 +72,4 @@ function putData(data) { function failData() { "use strict"; - console.log('Failed to get box!'); } \ No newline at end of file diff --git a/public/js/ff/piggy-banks/create.js b/public/js/ff/piggy-banks/create.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/piggy-banks/create.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/piggy-banks/edit.js b/public/js/ff/piggy-banks/edit.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/piggy-banks/edit.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/piggy-banks/index.js b/public/js/ff/piggy-banks/index.js index 39f516b01b..70630ad6d5 100644 --- a/public/js/ff/piggy-banks/index.js +++ b/public/js/ff/piggy-banks/index.js @@ -2,13 +2,12 @@ * index.js * Copyright (C) 2016 thegrumpydictator@gmail.com * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * 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. */ -/* globals $, lineChart, token, piggyBankID */ - -// Return a helper with preserved width of cells var fixHelper = function (e, tr) { "use strict"; var $originals = tr.children(); @@ -25,7 +24,7 @@ $(function () { $('.addMoney').on('click', addMoney); $('.removeMoney').on('click', removeMoney); - $('#sortable tbody').sortable( + $('#sortable-piggy tbody').sortable( { helper: fixHelper, stop: stopSorting, @@ -74,12 +73,12 @@ function stopSorting() { "use strict"; $('.loadSpin').addClass('fa fa-refresh fa-spin'); var order = []; - $.each($('#sortable>tbody>tr'), function (i, v) { + $.each($('#sortable-piggy>tbody>tr'), function (i, v) { var holder = $(v); var id = holder.data('id'); order.push(id); }); - $.post('piggy-banks/sort', {_token: token, order: order}).done(function () { + $.post('piggy-banks/sort', {order: order}).done(function () { $('.loadSpin').removeClass('fa fa-refresh fa-spin'); }); } \ No newline at end of file diff --git a/public/js/ff/piggy-banks/show.js b/public/js/ff/piggy-banks/show.js index 72cebea8ff..be5093f6ba 100644 --- a/public/js/ff/piggy-banks/show.js +++ b/public/js/ff/piggy-banks/show.js @@ -2,11 +2,12 @@ * show.js * Copyright (C) 2016 thegrumpydictator@gmail.com * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * 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. */ - -/* globals $, lineChart, piggyBankID */ +/** global: piggyBankID, lineChart */ $(function () { "use strict"; diff --git a/public/js/ff/preferences/index.js b/public/js/ff/preferences/index.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/preferences/index.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/reports/audit/all.js b/public/js/ff/reports/audit/all.js index f9f5a79f41..1724789bf4 100644 --- a/public/js/ff/reports/audit/all.js +++ b/public/js/ff/reports/audit/all.js @@ -2,11 +2,13 @@ * all.js * Copyright (C) 2016 thegrumpydictator@gmail.com * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * 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. */ -/* globals hideable */ +/** global: hideable */ $(function () { "use strict"; @@ -18,7 +20,6 @@ $(function () { arr.forEach(function (val) { $('input[type="checkbox"][value="' + val + '"]').prop('checked', true); }); - console.log('arr from cookie is ' + arr) } else { // no cookie? read list, store in array 'arr' // all account ids: @@ -45,7 +46,6 @@ function clickColumnOption() { function storeCheckboxes(checkboxes) { "use strict"; // store new cookie with those options: - console.log('Store new cookie with those options: ' + checkboxes); createCookie('audit-option-checkbox', checkboxes, 365); } @@ -59,7 +59,6 @@ function readCheckboxes() { checkboxes.push(c.val()); } }); - console.log('arr is now (default): ' + checkboxes); return checkboxes; } @@ -68,12 +67,10 @@ function showOnlyColumns(checkboxes) { for (var i = 0; i < hideable.length; i++) { var opt = hideable[i]; - if(checkboxes.indexOf(opt) > -1) { - console.log(opt + ' is in checkboxes'); + if (checkboxes.indexOf(opt) > -1) { $('td.hide-' + opt).show(); $('th.hide-' + opt).show(); } else { - console.log(opt + ' is NOT in checkboxes'); $('th.hide-' + opt).hide(); $('td.hide-' + opt).hide(); } @@ -101,8 +98,12 @@ function readCookie(name) { var ca = document.cookie.split(';'); for (var i = 0; i < ca.length; i++) { var c = ca[i]; - while (c.charAt(0) === ' ') c = c.substring(1, c.length); - if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length, c.length)); + while (c.charAt(0) === ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } } return null; } diff --git a/resources/lang/zh-HK/pagination.php b/public/js/ff/reports/budget/all.js similarity index 67% rename from resources/lang/zh-HK/pagination.php rename to public/js/ff/reports/budget/all.js index 9e61a6cfcc..25a412d1c5 100644 --- a/resources/lang/zh-HK/pagination.php +++ b/public/js/ff/reports/budget/all.js @@ -1,6 +1,5 @@ - '« Previous', - 'next' => 'Next »', - -]; diff --git a/public/js/ff/reports/budget/month.js b/public/js/ff/reports/budget/month.js new file mode 100644 index 0000000000..14d845fb6e --- /dev/null +++ b/public/js/ff/reports/budget/month.js @@ -0,0 +1,54 @@ +/* + * month.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: budgetExpenseUri, accountExpenseUri, mainUri */ + +$(function () { + "use strict"; + drawChart(); + + $('#budgets-out-pie-chart-checked').on('change', function () { + redrawPieChart('budgets-out-pie-chart', budgetExpenseUri); + }); + + $('#accounts-out-pie-chart-checked').on('change', function () { + redrawPieChart('accounts-out-pie-chart', accountExpenseUri); + }); + +}); + + +function drawChart() { + "use strict"; + + // month view: + doubleYNonStackedChart(mainUri, 'in-out-chart'); + + // draw pie chart of income, depending on "show other transactions too": + redrawPieChart('budgets-out-pie-chart', budgetExpenseUri); + redrawPieChart('accounts-out-pie-chart', accountExpenseUri); + + +} + +function redrawPieChart(container, uri) { + "use strict"; + var checkbox = $('#' + container + '-checked'); + + var others = '0'; + // check if box is checked: + if (checkbox.prop('checked')) { + others = '1'; + } + uri = uri.replace('OTHERS', others); + + pieChart(uri, container); + +} diff --git a/resources/lang/zh-TW/pagination.php b/public/js/ff/reports/category/all.js similarity index 66% rename from resources/lang/zh-TW/pagination.php rename to public/js/ff/reports/category/all.js index d87b6631ad..25a412d1c5 100644 --- a/resources/lang/zh-TW/pagination.php +++ b/public/js/ff/reports/category/all.js @@ -1,6 +1,5 @@ - '« 上一頁', - 'next' => '下一頁 »', - -]; diff --git a/public/js/ff/reports/category/month.js b/public/js/ff/reports/category/month.js new file mode 100644 index 0000000000..84f3444d9b --- /dev/null +++ b/public/js/ff/reports/category/month.js @@ -0,0 +1,64 @@ +/* + * month.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: categoryIncomeUri, categoryExpenseUri, accountIncomeUri, accountExpenseUri, mainUri */ + +$(function () { + "use strict"; + drawChart(); + + $('#categories-in-pie-chart-checked').on('change', function () { + redrawPieChart('categories-in-pie-chart', categoryIncomeUri); + }); + + $('#categories-out-pie-chart-checked').on('change', function () { + redrawPieChart('categories-out-pie-chart', categoryExpenseUri); + }); + + $('#accounts-in-pie-chart-checked').on('change', function () { + redrawPieChart('accounts-in-pie-chart', accountIncomeUri); + }); + + $('#accounts-out-pie-chart-checked').on('change', function () { + redrawPieChart('accounts-out-pie-chart', accountExpenseUri); + }); + +}); + + +function drawChart() { + "use strict"; + + // month view: + 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); + + +} + +function redrawPieChart(container, uri) { + "use strict"; + var checkbox = $('#' + container + '-checked'); + + var others = '0'; + // check if box is checked: + if (checkbox.prop('checked')) { + others = '1'; + } + uri = uri.replace('OTHERS', others); + + pieChart(uri, container); + +} diff --git a/public/js/ff/reports/default/all.js b/public/js/ff/reports/default/all.js index a76d6be0ec..1f838a0ebc 100644 --- a/public/js/ff/reports/default/all.js +++ b/public/js/ff/reports/default/all.js @@ -1,99 +1,33 @@ -/* globals startDate, showOnlyTop, showFullList, endDate, reportType, accountIds, inOutReportUrl, accountReportUrl */ /* * all.js * Copyright (C) 2016 thegrumpydictator@gmail.com * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * 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: accountReportUri, incomeReportUri, expenseReportUri, incExpReportUri, startDate, endDate, accountIds */ + $(function () { "use strict"; - // load the account report, which this report shows: - loadAccountReport(); + loadAjaxPartial('accountReport', accountReportUri); - // load income / expense / difference: - loadInOutReport(); - - // trigger info click - triggerInfoClick(); - - // trigger list length things: - listLengthInitial(); + // load income and expense reports: + loadAjaxPartial('incomeReport', incomeReportUri); + loadAjaxPartial('expenseReport', expenseReportUri); + loadAjaxPartial('incomeVsExpenseReport', incExpReportUri); }); function triggerInfoClick() { "use strict"; // find the little info buttons and respond to them. - $('.firefly-info-button').unbind('clicl').click(clickInfoButton); -} - -function listLengthInitial() { - "use strict"; - $('.overListLength').hide(); - $('.listLengthTrigger').unbind('click').click(triggerList) -} - -function triggerList(e) { - "use strict"; - var link = $(e.target); - var table = link.parent().parent().parent().parent(); - console.log('data-hidden = ' + table.attr('data-hidden')); - if (table.attr('data-hidden') === 'no') { - // hide all elements, return false. - table.find('.overListLength').hide(); - table.attr('data-hidden', 'yes'); - link.text(showFullList); - return false; - } - // show all, return false - table.find('.overListLength').show(); - table.attr('data-hidden', 'no'); - link.text(showOnlyTop); - - return false; -} - -function loadInOutReport() { - "use strict"; - console.log('Going to grab ' + inOutReportUrl); - $.get(inOutReportUrl).done(placeInOutReport).fail(failInOutReport); -} - -function placeInOutReport(data) { - "use strict"; - $('#incomeReport').removeClass('loading').html(data.income); - $('#expenseReport').removeClass('loading').html(data.expenses); - $('#incomeVsExpenseReport').removeClass('loading').html(data.incomes_expenses); - listLengthInitial(); - triggerInfoClick(); -} - -function failInOutReport() { - "use strict"; - console.log('Fail in/out report data!'); - $('#incomeReport').removeClass('loading').addClass('general-chart-error'); - $('#expenseReport').removeClass('loading').addClass('general-chart-error'); - $('#incomeVsExpenseReport').removeClass('loading').addClass('general-chart-error'); -} - -function loadAccountReport() { - "use strict"; - $.get(accountReportUrl).done(placeAccountReport).fail(failAccountReport); -} - -function placeAccountReport(data) { - "use strict"; - $('#accountReport').removeClass('loading').html(data); -} - -function failAccountReport(data) { - "use strict"; - $('#accountReport').removeClass('loading').addClass('general-chart-error'); + $('.firefly-info-button').unbind('click').click(clickInfoButton); } function clickInfoButton(e) { @@ -108,13 +42,12 @@ function clickInfoButton(e) { // add some more elements: attributes.startDate = startDate; attributes.endDate = endDate; - attributes.reportType = reportType; attributes.accounts = accountIds; - $.getJSON('popup/report', {attributes: attributes}).done(respondInfoButton).fail(errorInfoButton); + $.getJSON('popup/general', {attributes: attributes}).done(respondInfoButton).fail(errorInfoButton); } -function errorInfoButton(data) { +function errorInfoButton() { "use strict"; // remove wait cursor $('body').removeClass('waiting'); @@ -125,7 +58,71 @@ function respondInfoButton(data) { "use strict"; // remove wait cursor $('body').removeClass('waiting'); - $('#defaultModal').empty().html(data.html); - $('#defaultModal').modal('show'); + $('#defaultModal').empty().html(data.html).modal('show'); +} + +function loadAjaxPartial(holder, uri) { + "use strict"; + $.get(uri).done(function (data) { + displayAjaxPartial(data, holder); + }).fail(function () { + failAjaxPartial(uri, holder); + }); +} + +function displayAjaxPartial(data, holder) { + "use strict"; + var obj = $('#' + holder); + obj.html(data); + obj.parent().find('.overlay').remove(); + + // call some often needed recalculations and what-not: + + // find a sortable table and make it sortable: + if (typeof $.bootstrapSortable === "function") { + $.bootstrapSortable(true); + } + + // find the info click things and respond to them: + triggerInfoClick(); + + // trigger list thing + listLengthInitial(); + + // budget thing in year and multi year report: + $('.budget-chart-activate').unbind('click').on('click', clickBudgetChart); + + // category thing in year and multi year report: + $('.category-chart-activate').unbind('click').on('click', clickCategoryChart); +} + +function failAjaxPartial(uri, holder) { + "use strict"; + var holder = $('#' + holder); + holder.parent().find('.overlay').remove(); + holder.addClass('general-chart-error'); + +} + +function clickCategoryChart(e) { + "use strict"; + var link = $(e.target); + var categoryId = link.data('category'); + + var URL = 'chart/category/report-period/' + categoryId + '/' + accountIds + '/' + startDate + '/' + endDate; + var container = 'category_chart'; + columnChart(URL, container); + return false; +} + +function clickBudgetChart(e) { + "use strict"; + var link = $(e.target); + var budgetId = link.data('budget'); + + var URL = 'chart/budget/period/' + budgetId + '/' + accountIds + '/' + startDate + '/' + endDate; + var container = 'budget_chart'; + columnChart(URL, container); + return false; } \ No newline at end of file diff --git a/public/js/ff/reports/default/month.js b/public/js/ff/reports/default/month.js index d9f30b1f8c..1c2f08be63 100644 --- a/public/js/ff/reports/default/month.js +++ b/public/js/ff/reports/default/month.js @@ -1,57 +1,28 @@ -/* globals google, startDate ,reportURL, endDate , reportType ,accountIds, lineChart, categoryReportUrl, balanceReportUrl */ +/* + * month.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ +/** global: categoryReportUri, budgetReportUri, balanceReportUri, accountChartUri */ $(function () { "use strict"; drawChart(); - loadCategoryReport(); - loadBalanceReport(); + loadAjaxPartial('categoryReport', categoryReportUri); + loadAjaxPartial('budgetReport', budgetReportUri); + loadAjaxPartial('balanceReport',balanceReportUri); }); -function loadCategoryReport() { - "use strict"; - console.log('Going to grab ' + categoryReportUrl); - $.get(categoryReportUrl).done(placeCategoryReport).fail(failCategoryReport); -} - -function loadBalanceReport() { - "use strict"; - console.log('Going to grab ' + categoryReportUrl); - $.get(balanceReportUrl).done(placeBalanceReport).fail(failBalanceReport); -} - -function placeBalanceReport(data) { - "use strict"; - $('#balanceReport').removeClass('loading').html(data); - listLengthInitial(); - triggerInfoClick(); -} - -function placeCategoryReport(data) { - "use strict"; - $('#categoryReport').removeClass('loading').html(data); - listLengthInitial(); - triggerInfoClick(); -} - -function failBalanceReport() { - "use strict"; - console.log('Fail balance report data!'); - $('#balanceReport').removeClass('loading').addClass('general-chart-error'); -} - -function failCategoryReport() { - "use strict"; - console.log('Fail category report data!'); - $('#categoryReport').removeClass('loading').addClass('general-chart-error'); -} - - function drawChart() { "use strict"; // month view: // draw account chart - lineChart('chart/account/report/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'account-balances-chart'); + lineChart(accountChartUri, 'account-balances-chart'); } \ No newline at end of file diff --git a/public/js/ff/reports/default/multi-year.js b/public/js/ff/reports/default/multi-year.js index 78845f5100..1bd49a294d 100644 --- a/public/js/ff/reports/default/multi-year.js +++ b/public/js/ff/reports/default/multi-year.js @@ -1,156 +1,29 @@ -/* globals google, startDate ,reportURL, endDate , reportType ,accountIds , picker:true, minDate, year, month, columnChart, lineChart, stackedColumnChart */ +/* + * multi-year.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ +/** global: budgetPeriodReportUri, categoryExpenseUri, categoryIncomeUri, netWorthUri, opChartUri, sumChartUri */ $(function () { "use strict"; drawChart(); + loadAjaxPartial('budgetPeriodReport', budgetPeriodReportUri); + loadAjaxPartial('categoryExpense', categoryExpenseUri); + loadAjaxPartial('categoryIncome', categoryIncomeUri); }); - function drawChart() { "use strict"; // income and expense over multi year: - lineChart('chart/report/net-worth/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'net-worth'); - columnChart('chart/report/in-out/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'income-expenses-chart'); - columnChart('chart/report/in-out-sum/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'income-expenses-sum-chart'); - - - $.each($('.account-chart'), function (i, v) { - var holder = $(v); - console.log('Will draw chart for account #' + holder.data('id')); - }); - - // draw budget chart based on selected budgets: - $('.budget-checkbox').on('change', updateBudgetChart); - selectBudgetsByCookie(); - updateBudgetChart(); - - // draw category chart based on selected budgets: - $('.category-checkbox').on('change', updateCategoryChart); - selectCategoriesByCookie(); - updateCategoryChart(); + lineChart(netWorthUri, 'net-worth'); + columnChart(opChartUri, 'income-expenses-chart'); + columnChart(sumChartUri, 'income-expenses-sum-chart'); } - -function selectBudgetsByCookie() { - "use strict"; - var cookie = readCookie('multi-year-budgets'); - if (cookie !== null) { - var cookieArray = cookie.split(','); - for (var x in cookieArray) { - var budgetId = cookieArray[x]; - $('.budget-checkbox[value="' + budgetId + '"').prop('checked', true); - } - } -} - -function selectCategoriesByCookie() { - "use strict"; - var cookie = readCookie('multi-year-categories'); - if (cookie !== null) { - var cookieArray = cookie.split(','); - for (var x in cookieArray) { - var categoryId = cookieArray[x]; - $('.category-checkbox[value="' + categoryId + '"').prop('checked', true); - } - } -} - -function updateBudgetChart() { - "use strict"; - console.log('will update budget chart.'); - // get all budget ids: - var budgets = []; - $.each($('.budget-checkbox'), function (i, v) { - var current = $(v); - if (current.prop('checked')) { - budgets.push(current.val()); - } - }); - - if (budgets.length > 0) { - - var budgetIds = budgets.join(','); - - // remove old chart: - $('#budgets-chart').replaceWith(''); - - // hide message: - $('#budgets-chart-message').hide(); - - // draw chart. Redraw when exists? Not sure if we support that. - columnChart('chart/budget/multi-year/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds + '/' + budgetIds, 'budgets-chart'); - createCookie('multi-year-budgets', budgets, 365); - } else { - // hide canvas, show message: - $('#budgets-chart-message').show(); - $('#budgets-chart').hide(); - - } - -} - -function updateCategoryChart() { - "use strict"; - console.log('will update category chart.'); - // get all category ids: - var categories = []; - $.each($('.category-checkbox'), function (i, v) { - var current = $(v); - if (current.prop('checked')) { - categories.push(current.val()); - } - }); - - if (categories.length > 0) { - - var categoryIds = categories.join(','); - - // remove old chart: - $('#categories-chart').replaceWith(''); - - // hide message: - $('#categories-chart-message').hide(); - - // draw chart. Redraw when exists? Not sure if we support that. - columnChart('chart/category/multi-year/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds + '/' + categoryIds, 'categories-chart'); - createCookie('multi-year-categories', categories, 365); - } else { - // hide canvas, show message: - $('#categories-chart-message').show(); - $('#categories-chart').hide(); - - } -} - - -function createCookie(name, value, days) { - "use strict"; - var expires; - - if (days) { - var date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); - expires = "; expires=" + date.toGMTString(); - } else { - expires = ""; - } - document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + expires + "; path=/"; -} - -function readCookie(name) { - "use strict"; - var nameEQ = encodeURIComponent(name) + "="; - var ca = document.cookie.split(';'); - for (var i = 0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0) === ' ') c = c.substring(1, c.length); - if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length, c.length)); - } - return null; -} - -function eraseCookie(name) { - createCookie(name, "", -1); -} \ No newline at end of file diff --git a/public/js/ff/reports/default/year.js b/public/js/ff/reports/default/year.js index 3afa11d242..c1eb1070f3 100644 --- a/public/js/ff/reports/default/year.js +++ b/public/js/ff/reports/default/year.js @@ -1,86 +1,30 @@ -/* globals google, startDate ,reportURL, endDate , reportType ,accountIds , picker:true, minDate, year, month, columnChart, lineChart, stackedColumnChart */ +/* + * year.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: budgetPeriodReportUri, categoryExpenseUri, categoryIncomeUri, netWorthUri, opChartUri, sumChartUri */ -var chartDrawn; -var budgetChart; $(function () { "use strict"; - chartDrawn = false; drawChart(); + loadAjaxPartial('budgetPeriodReport', budgetPeriodReportUri); + loadAjaxPartial('categoryExpense', categoryExpenseUri); + loadAjaxPartial('categoryIncome', categoryIncomeUri); }); - function drawChart() { "use strict"; - lineChart('chart/report/net-worth/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'net-worth'); - columnChart('chart/report/in-out/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'income-expenses-chart'); - columnChart('chart/report/in-out-sum/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'income-expenses-sum-chart'); + lineChart(netWorthUri, 'net-worth'); + columnChart(opChartUri, 'income-expenses-chart'); + columnChart(sumChartUri, 'income-expenses-sum-chart'); + - $('.budget-chart-activate').on('click', clickBudgetChart); } - -function clickBudgetChart(e) { - "use strict"; - var link = $(e.target); - var budgetId = link.data('budget'); - var URL = 'chart/budget/period/' + budgetId + '/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds; - var container = 'budget_chart'; - // if chart drawn is false, draw the first one, then - // set to true - if (chartDrawn == false) { - // do new chart: - - - $.getJSON(URL).done(function (data) { - console.log('Will draw new columnChart(' + URL + ')'); - - var ctx = document.getElementById(container).getContext("2d"); - var newData = {}; - newData.datasets = []; - - for (var i = 0; i < data.count; i++) { - newData.labels = data.labels; - var dataset = data.datasets[i]; - dataset.backgroundColor = fillColors[i]; - newData.datasets.push(dataset); - } - // completely new chart. - budgetChart = new Chart(ctx, { - type: 'bar', - data: data, - options: defaultColumnOptions - }); - - }).fail(function () { - $('#' + container).addClass('general-chart-error'); - }); - console.log('URL for column chart : ' + URL); - chartDrawn = true; - } else { - console.log('Will now handle remove data and add new!'); - $.getJSON(URL).done(function (data) { - console.log('Will draw updated columnChart(' + URL + ')'); - var newData = {}; - newData.datasets = []; - - for (var i = 0; i < data.count; i++) { - newData.labels = data.labels; - var dataset = data.datasets[i]; - dataset.backgroundColor = fillColors[i]; - newData.datasets.push(dataset); - } - // update the chart - console.log('Now update chart thing.'); - budgetChart.data.datasets = newData.datasets; - budgetChart.update(); - - }).fail(function () { - $('#' + container).addClass('general-chart-error'); - }); - - - } - - return false; -} \ No newline at end of file diff --git a/public/js/ff/reports/index.js b/public/js/ff/reports/index.js index 8abb613f25..df65b9704f 100644 --- a/public/js/ff/reports/index.js +++ b/public/js/ff/reports/index.js @@ -1,12 +1,31 @@ -/* globals google, startDate ,reportURL, endDate , reportType ,accountIds , picker:true, minDate, year, month, columnChart, lineChart, stackedColumnChart */ +/* + * index.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ +/** global: minDate, nonSelectedText, allSelectedText, filterPlaceholder */ + +var defaultMultiSelect = { + disableIfEmpty: true, + nonSelectedText: nonSelectedText, + allSelectedText: allSelectedText, + includeSelectAllOption: true, + enableFiltering: true, + enableCaseInsensitiveFiltering: true, + filterPlaceholder: filterPlaceholder +}; $(function () { "use strict"; if ($('#inputDateRange').length > 0) { - picker = $('#inputDateRange').daterangepicker( + $('#inputDateRange').daterangepicker( { locale: { format: 'YYYY-MM-DD', @@ -18,19 +37,23 @@ $(function () { ); - // set values from cookies, if any: - if (readCookie('report-type') !== null) { + // set report type from cookie, if any: + if (!(readCookie('report-type') === null)) { $('select[name="report_type"]').val(readCookie('report-type')); } + // set accounts from cookie if ((readCookie('report-accounts') !== null)) { var arr = readCookie('report-accounts').split(','); arr.forEach(function (val) { - $('input[type="checkbox"][value="' + val + '"]').prop('checked', true); + $('#inputAccounts').find('option[value="' + val + '"]').prop('selected', true); }); } - // set date: + // make account select a hip new bootstrap multi-select thing. + $('#inputAccounts').multiselect(defaultMultiSelect); + + // set date from cookie var startStr = readCookie('report-start'); var endStr = readCookie('report-end'); if (startStr !== null && endStr !== null && startStr.length == 8 && endStr.length == 8) { @@ -44,43 +67,81 @@ $(function () { $('.date-select').on('click', preSelectDate); $('#report-form').on('submit', catchSubmit); + $('select[name="report_type"]').on('change', getReportOptions); + getReportOptions(); }); +function getReportOptions() { + "use strict"; + var reportType = $('select[name="report_type"]').val(); + var boxBody = $('#extra-options'); + var box = $('#extra-options-box'); + boxBody.empty(); + box.find('.overlay').show(); + + $.getJSON('reports/options/' + reportType, function (data) { + boxBody.html(data.html); + setOptionalFromCookies(); + box.find('.overlay').hide(); + }).fail(function () { + boxBody.addClass('error'); + box.find('.overlay').hide(); + }); +} + +function setOptionalFromCookies() { + var arr; + // categories + if ((readCookie('report-categories') !== null)) { + arr = readCookie('report-categories').split(','); + arr.forEach(function (val) { + $('#inputCategories').find('option[value="' + val + '"]').prop('selected', true); + }); + } + $('#inputCategories').multiselect(defaultMultiSelect); + + // and budgets! + if ((readCookie('report-budgets') !== null)) { + arr = readCookie('report-budgets').split(','); + arr.forEach(function (val) { + $('#inputBudgets').find('option[value="' + val + '"]').prop('selected', true); + }); + } + $('#inputBudgets').multiselect(defaultMultiSelect); + + // and tags! + if ((readCookie('report-tags') !== null)) { + arr = readCookie('report-tags').split(','); + arr.forEach(function (val) { + $('#inputBudgets').find('option[value="' + val + '"]').prop('selected', true); + }); + } + $('#inputTags').multiselect(defaultMultiSelect); +} + function catchSubmit() { "use strict"; - // default;20141201;20141231;4;5 - // report name: - var url = '' + $('select[name="report_type"]').val() + '/'; - // date, processed: var picker = $('#inputDateRange').data('daterangepicker'); - url += moment(picker.startDate).format("YYYYMMDD") + '/'; - url += moment(picker.endDate).format("YYYYMMDD") + '/'; // all account ids: - var count = 0; - var accounts = []; - $.each($('.account-checkbox'), function (i, v) { - var c = $(v); - if (c.prop('checked')) { - url += c.val() + ','; - accounts.push(c.val()); - count++; - } - }); - if (count > 0) { - // set cookie to remember choices. - createCookie('report-type', $('select[name="report_type"]').val(), 365); - createCookie('report-accounts', accounts, 365); - createCookie('report-start', moment(picker.startDate).format("YYYYMMDD"), 365); - createCookie('report-end', moment(picker.endDate).format("YYYYMMDD"), 365); + var accounts = $('#inputAccounts').val(); + var categories = $('#inputCategories').val(); + var budgets = $('#inputBudgets').val(); + var tags = $('#inputTags').val(); - window.location.href = reportURL + "/" + url; - } - //console.log(url); + // remember all + // set cookie to remember choices. + createCookie('report-type', $('select[name="report_type"]').val(), 365); + createCookie('report-accounts', accounts, 365); + createCookie('report-categories', categories, 365); + createCookie('report-budgets', budgets, 365); + createCookie('report-tags', tags, 365); + createCookie('report-start', moment(picker.startDate).format("YYYYMMDD"), 365); + createCookie('report-end', moment(picker.endDate).format("YYYYMMDD"), 365); - return false; + return true; } function preSelectDate(e) { @@ -114,8 +175,12 @@ function readCookie(name) { var ca = document.cookie.split(';'); for (var i = 0; i < ca.length; i++) { var c = ca[i]; - while (c.charAt(0) === ' ') c = c.substring(1, c.length); - if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length, c.length)); + while (c.charAt(0) === ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } } return null; } diff --git a/public/js/ff/rules/create-edit.js b/public/js/ff/rules/create-edit.js index 9e836eb3d6..9100050012 100644 --- a/public/js/ff/rules/create-edit.js +++ b/public/js/ff/rules/create-edit.js @@ -2,113 +2,307 @@ * create-edit.js * Copyright (C) 2016 thegrumpydictator@gmail.com * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * 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. */ -var triggerCount = 0; -var actionCount = 0; +/** global: triggerCount, actionCount */ $(function () { "use strict"; - console.log('edit-create'); + if (triggerCount === 0) { + addNewTrigger(); + } + if (actionCount === 0) { + addNewAction(); + } + if (triggerCount > 0) { + onAddNewTrigger(); + } + + if (actionCount > 0) { + onAddNewAction(); + } + + $('.add_rule_trigger').click(addNewTrigger); + $('.add_rule_action').click(addNewAction); + $('.test_rule_triggers').click(testRuleTriggers); + $('.remove-trigger').unbind('click').click(removeTrigger); + $('.remove-action').unbind('click').click(removeAction); }); - +/** + * This method triggers when a new trigger must be added to the form. + */ function addNewTrigger() { "use strict"; triggerCount++; + // disable the button + $('.add_rule_trigger').attr('disabled', 'disabled'); + + // get the HTML for the new trigger $.getJSON('json/trigger', {count: triggerCount}).done(function (data) { + + // append it to the other triggers $('tbody.rule-trigger-tbody').append(data.html); + $('.remove-trigger').unbind('click').click(removeTrigger); - $('.remove-trigger').unbind('click').click(function (e) { - removeTrigger(e); + // update all "select trigger type" dropdowns + // so the accompanying text-box has the correct autocomplete. + onAddNewTrigger(); + + $('.add_rule_trigger').removeAttr('disabled'); - return false; - }); }).fail(function () { alert('Cannot get a new trigger.'); + $('.add_rule_trigger').removeAttr('disabled'); }); + return false; } +/** + * Method triggers when a new action must be added to the form.. + */ function addNewAction() { "use strict"; actionCount++; + // disable the button + $('.add_rule_action').attr('disabled', 'disabled'); + + $.getJSON('json/action', {count: actionCount}).done(function (data) { - //console.log(data.html); $('tbody.rule-action-tbody').append(data.html); // add action things. - $('.remove-action').unbind('click').click(function (e) { - removeAction(e); + $('.remove-action').unbind('click').click(removeAction); - return false; - }); + // update all "select trigger type" dropdowns + // so the accompanying text-box has the correct autocomplete. + onAddNewAction(); + + $('.add_rule_action').removeAttr('disabled'); }).fail(function () { alert('Cannot get a new action.'); + + $('.add_rule_action').removeAttr('disabled'); }); + return false; } +/** + * Method fires when a trigger must be removed from the form. + * + * @param e + */ function removeTrigger(e) { "use strict"; var target = $(e.target); - if(target.prop("tagName") == "I") { + if (target.prop("tagName") == "I") { target = target.parent(); } // remove grand parent: target.parent().parent().remove(); // if now at zero, immediatly add one again: - if($('.rule-trigger-tbody tr').length == 0) { + if ($('.rule-trigger-tbody tr').length == 0) { addNewTrigger(); } + return false; } +/** + * Method fires when an action must be removed from the form. + * + * @param e + */ function removeAction(e) { "use strict"; var target = $(e.target); - if(target.prop("tagName") == "I") { + if (target.prop("tagName") == "I") { target = target.parent(); } // remove grand parent: target.parent().parent().remove(); // if now at zero, immediatly add one again: - if($('.rule-action-tbody tr').length == 0) { + if ($('.rule-action-tbody tr').length == 0) { addNewAction(); } + return false; + +} + +/** + * Method fires when a new action is added. It will update ALL action value input fields. + */ +function onAddNewAction() { + "use strict"; + + // update all "select action type" dropdown buttons so they will respond correctly + $('select[name^="rule-action["]').unbind('change').change(function (e) { + var target = $(e.target); + updateActionInput(target) + }); + + $.each($('.rule-action-holder'), function (i, v) { + var holder = $(v); + var select = holder.find('select'); + updateActionInput(select); + }); +} + +/** + * Method fires when a new trigger is added. It will update ALL trigger value input fields. + */ +function onAddNewTrigger() { + "use strict"; + + // update all "select trigger type" dropdown buttons so they will respond correctly + $('select[name^="rule-trigger["]').unbind('change').change(function (e) { + var target = $(e.target); + updateTriggerInput(target) + }); + + $.each($('.rule-trigger-holder'), function (i, v) { + var holder = $(v); + var select = holder.find('select'); + updateTriggerInput(select); + }); +} + +/** + * Creates a nice auto complete action depending on the type of the select list value thing. + * + * @param selectList + */ +function updateActionInput(selectList) { + // the actual row this select list is in: + var parent = selectList.parent().parent(); + // the text input we're looking for: + var input = parent.find('input[name^="rule-action-value["]'); + input.removeAttr('disabled'); + switch (selectList.val()) { + case 'set_category': + createAutoComplete(input, 'json/categories'); + break; + case 'clear_category': + case 'clear_budget': + case 'remove_all_tags': + input.attr('disabled', 'disabled'); + break; + case 'set_budget': + createAutoComplete(input, 'json/budgets'); + break; + case 'add_tag': + case 'remove_tag': + createAutoComplete(input, 'json/tags'); + break; + case 'set_description': + createAutoComplete(input, 'json/transaction-journals/all'); + break; + case 'set_source_account': + createAutoComplete(input, 'json/all-accounts'); + break; + case 'set_destination_account': + createAutoComplete(input, 'json/all-accounts'); + break; + default: + input.typeahead('destroy'); + break; + } +} + +/** + * Creates a nice auto complete trigger depending on the type of the select list value thing. + * + * @param selectList + */ +function updateTriggerInput(selectList) { + // the actual row this select list is in: + var parent = selectList.parent().parent(); + // the text input we're looking for: + var input = parent.find('input[name^="rule-trigger-value["]'); + switch (selectList.val()) { + case 'from_account_starts': + case 'from_account_ends': + case 'from_account_is': + case 'from_account_contains': + case 'to_account_starts': + case 'to_account_ends': + case 'to_account_is': + case 'to_account_contains': + createAutoComplete(input, 'json/all-accounts'); + break; + case 'tag_is': + // also make tag thing? + createAutoComplete(input, 'json/tags'); + break; + case 'budget_is': + createAutoComplete(input, 'json/budgets'); + break; + case 'category_is': + createAutoComplete(input, 'json/categories'); + break; + case 'transaction_type': + createAutoComplete(input, 'json/transaction-types'); + break; + case 'description_starts': + case 'description_ends': + case 'description_contains': + case 'description_is': + createAutoComplete(input, 'json/transaction-journals/all'); + break; + default: + input.typeahead('destroy'); + break; + } +} + +/** + * Create actual autocomplete + * @param input + * @param URI + */ +function createAutoComplete(input, URI) { + input.typeahead('destroy'); + $.getJSON(URI).done(function (data) { + input.typeahead({source: data}); + }); + } function testRuleTriggers() { - "use strict"; - - // Serialize all trigger data - var triggerData = $( ".rule-trigger-tbody" ).find( "input[type=text], input[type=checkbox], select" ).serializeArray(); - - // Find a list of existing transactions that match these triggers + "use strict"; + + // Serialize all trigger data + var triggerData = $(".rule-trigger-tbody").find("input[type=text], input[type=checkbox], select").serializeArray(); + + // Find a list of existing transactions that match these triggers $.get('rules/test', triggerData).done(function (data) { - var modal = $( "#testTriggerModal" ); - var numTriggers = $( ".rule-trigger-body > tr" ).length; - - // Set title and body - modal.find( ".transactions-list" ).html(data.html); - - // Show warning if appropriate - if( data.warning ) { - modal.find( ".transaction-warning .warning-contents" ).text(data.warning); - modal.find( ".transaction-warning" ).show(); - } else { - modal.find( ".transaction-warning" ).hide(); - } - - // Show the modal dialog - $( "#testTriggerModal" ).modal(); + var modal = $("#testTriggerModal"); + + // Set title and body + modal.find(".transactions-list").html(data.html); + + // Show warning if appropriate + if (data.warning) { + modal.find(".transaction-warning .warning-contents").text(data.warning); + modal.find(".transaction-warning").show(); + } else { + modal.find(".transaction-warning").hide(); + } + + // Show the modal dialog + $("#testTriggerModal").modal(); }).fail(function () { alert('Cannot get transactions for given triggers.'); }); + return false; } \ No newline at end of file diff --git a/public/js/ff/rules/create.js b/public/js/ff/rules/create.js deleted file mode 100644 index 857f5b2617..0000000000 --- a/public/js/ff/rules/create.js +++ /dev/null @@ -1,40 +0,0 @@ -/* global $, addNewTrigger, addNewAction, actionCount, triggerCount */ -/* - * edit.js - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -// make a line. - -$(function () { - "use strict"; - console.log("create"); - if (triggerCount === 0) { - addNewTrigger(); - } - if (actionCount === 0) { - addNewAction(); - } - - - $('.add_rule_trigger').click(function () { - addNewTrigger(); - - return false; - }); - - $('.add_rule_action').click(function () { - addNewAction(); - - return false; - }); - - $('.test_rule_triggers').click(function () { - testRuleTriggers(); - - return false; - }); -}); diff --git a/public/js/ff/rules/edit.js b/public/js/ff/rules/edit.js deleted file mode 100644 index c66a905d0c..0000000000 --- a/public/js/ff/rules/edit.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * edit.js - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -$(function () { - "use strict"; - console.log("edit"); - - if (triggerCount === 0) { - addNewTrigger(); - } - if (actionCount === 0) { - addNewAction(); - } - - - $('.add_rule_trigger').click(function () { - addNewTrigger(); - - return false; - }); - - $('.test_rule_triggers').click(function () { - testRuleTriggers(); - - return false; - }); - - $('.add_rule_action').click(function () { - addNewAction(); - - return false; - }); - - $('.remove-trigger').unbind('click').click(function (e) { - removeTrigger(e); - - return false; - }); - - // add action things. - $('.remove-action').unbind('click').click(function (e) { - removeAction(e); - - return false; - }); -}); \ No newline at end of file diff --git a/public/js/ff/rules/index.js b/public/js/ff/rules/index.js index 133f770a98..0fc0ef7371 100644 --- a/public/js/ff/rules/index.js +++ b/public/js/ff/rules/index.js @@ -1,13 +1,13 @@ -/* global comboChart,token, billID */ /* * index.js * Copyright (C) 2016 thegrumpydictator@gmail.com * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * 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. */ -// Return a helper with preserved width of cells var fixHelper = function (e, tr) { "use strict"; var $originals = tr.children(); @@ -48,21 +48,19 @@ function sortStop(event, ui) { var ruleId = current.parent().data('id'); var entries = []; // who am i? - console.log('Rule: #' + current.parent().data('id')); $.each(parent.children(), function (i, v) { var trigger = $(v); var id = trigger.data('id'); - var order = i + 1; entries.push(id); }); if (parent.hasClass('rule-triggers')) { - $.post('rules/trigger/order/' + ruleId, {_token: token, triggers: entries}).fail(function () { + $.post('rules/trigger/order/' + ruleId, {triggers: entries}).fail(function () { alert('Could not re-order rule triggers. Please refresh the page.'); }); } else { - $.post('rules/action/order/' + ruleId, {_token: token, actions: entries}).fail(function () { + $.post('rules/action/order/' + ruleId, {actions: entries}).fail(function () { alert('Could not re-order rule actions. Please refresh the page.'); }); diff --git a/public/js/ff/rules/select-transactions.js b/public/js/ff/rules/select-transactions.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/rules/select-transactions.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/tags/edit.js b/public/js/ff/tags/create-edit.js similarity index 76% rename from public/js/ff/tags/edit.js rename to public/js/ff/tags/create-edit.js index 6ea5731733..7184b17ae2 100644 --- a/public/js/ff/tags/edit.js +++ b/public/js/ff/tags/create-edit.js @@ -1,8 +1,23 @@ -/* globals zoomLevel, token, google, latitude, longitude, doPlaceMarker */ +/* + * create-edit.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ +/** global: zoomLevel, latitude, longitude, google, apiKey, doPlaceMarker, Modernizr */ + $(function () { "use strict"; $('#clearLocation').click(clearLocation); + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } }); @@ -51,12 +66,12 @@ function initialize() { Respond to zoom event. */ google.maps.event.addListener(map, 'zoom_changed', function () { - saveZoomLevel(event); + saveZoomLevel(); }); /* Maybe place marker? */ - if(doPlaceMarker) { + if(doPlaceMarker == true) { var myLatlng = new google.maps.LatLng(latitude,longitude); var fakeEvent = {}; fakeEvent.latLng = myLatlng; diff --git a/public/js/ff/tags/create.js b/public/js/ff/tags/create.js deleted file mode 100644 index 6ea5731733..0000000000 --- a/public/js/ff/tags/create.js +++ /dev/null @@ -1,104 +0,0 @@ -/* globals zoomLevel, token, google, latitude, longitude, doPlaceMarker */ -$(function () { - "use strict"; - - $('#clearLocation').click(clearLocation); - -}); - -/* - Some vars as prep for the map: - */ -var map; -var markers = []; -var setTag = false; - -var mapOptions = { - zoom: zoomLevel, - center: new google.maps.LatLng(latitude, longitude), - disableDefaultUI: true -}; - -/* - Clear location and reset zoomLevel. - */ -function clearLocation() { - "use strict"; - deleteMarkers(); - $('input[name="latitude"]').val(""); - $('input[name="longitude"]').val(""); - $('input[name="zoomLevel"]').val("6"); - setTag = false; - $('input[name="setTag"]').val('false'); - return false; -} - -function initialize() { - "use strict"; - /* - Create new map: - */ - map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions); - - /* - Respond to click event. - */ - google.maps.event.addListener(map, 'rightclick', function (event) { - placeMarker(event); - }); - - /* - Respond to zoom event. - */ - google.maps.event.addListener(map, 'zoom_changed', function () { - saveZoomLevel(event); - }); - /* - Maybe place marker? - */ - if(doPlaceMarker) { - var myLatlng = new google.maps.LatLng(latitude,longitude); - var fakeEvent = {}; - fakeEvent.latLng = myLatlng; - placeMarker(fakeEvent); - - } -} - -/** - * save zoom level of map into hidden input. - */ -function saveZoomLevel() { - "use strict"; - $('input[name="zoomLevel"]').val(map.getZoom()); -} - -/** - * Place marker on map. - * @param event - */ -function placeMarker(event) { - "use strict"; - deleteMarkers(); - var marker = new google.maps.Marker({position: event.latLng, map: map}); - $('input[name="latitude"]').val(event.latLng.lat()); - $('input[name="longitude"]').val(event.latLng.lng()); - markers.push(marker); - setTag = true; - $('input[name="setTag"]').val('true'); -} - - -/** - * Deletes all markers in the array by removing references to them. - */ -function deleteMarkers() { - "use strict"; - for (var i = 0; i < markers.length; i++) { - markers[i].setMap(null); - } - markers = []; -} - - -google.maps.event.addDomListener(window, 'load', initialize); \ No newline at end of file diff --git a/public/js/ff/tags/index.js b/public/js/ff/tags/index.js index 6ea5731733..990b265895 100644 --- a/public/js/ff/tags/index.js +++ b/public/js/ff/tags/index.js @@ -1,104 +1,15 @@ -/* globals zoomLevel, token, google, latitude, longitude, doPlaceMarker */ +/* + * index.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: zoomLevel, latitude, longitude, google */ + $(function () { "use strict"; - - $('#clearLocation').click(clearLocation); - }); - -/* - Some vars as prep for the map: - */ -var map; -var markers = []; -var setTag = false; - -var mapOptions = { - zoom: zoomLevel, - center: new google.maps.LatLng(latitude, longitude), - disableDefaultUI: true -}; - -/* - Clear location and reset zoomLevel. - */ -function clearLocation() { - "use strict"; - deleteMarkers(); - $('input[name="latitude"]').val(""); - $('input[name="longitude"]').val(""); - $('input[name="zoomLevel"]').val("6"); - setTag = false; - $('input[name="setTag"]').val('false'); - return false; -} - -function initialize() { - "use strict"; - /* - Create new map: - */ - map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions); - - /* - Respond to click event. - */ - google.maps.event.addListener(map, 'rightclick', function (event) { - placeMarker(event); - }); - - /* - Respond to zoom event. - */ - google.maps.event.addListener(map, 'zoom_changed', function () { - saveZoomLevel(event); - }); - /* - Maybe place marker? - */ - if(doPlaceMarker) { - var myLatlng = new google.maps.LatLng(latitude,longitude); - var fakeEvent = {}; - fakeEvent.latLng = myLatlng; - placeMarker(fakeEvent); - - } -} - -/** - * save zoom level of map into hidden input. - */ -function saveZoomLevel() { - "use strict"; - $('input[name="zoomLevel"]').val(map.getZoom()); -} - -/** - * Place marker on map. - * @param event - */ -function placeMarker(event) { - "use strict"; - deleteMarkers(); - var marker = new google.maps.Marker({position: event.latLng, map: map}); - $('input[name="latitude"]').val(event.latLng.lat()); - $('input[name="longitude"]').val(event.latLng.lng()); - markers.push(marker); - setTag = true; - $('input[name="setTag"]').val('true'); -} - - -/** - * Deletes all markers in the array by removing references to them. - */ -function deleteMarkers() { - "use strict"; - for (var i = 0; i < markers.length; i++) { - markers[i].setMap(null); - } - markers = []; -} - - -google.maps.event.addDomListener(window, 'load', initialize); \ No newline at end of file diff --git a/public/js/ff/transactions/create-edit.js b/public/js/ff/transactions/create-edit.js deleted file mode 100644 index 8a83c9968d..0000000000 --- a/public/js/ff/transactions/create-edit.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * create-edit.js - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ -/* globals what:true, $, doSwitch, txt, middleCrumbName, title,button, middleCrumbUrl, piggiesLength, breadcrumbs */ -$(document).ready(function () { - "use strict"; - - // the destination account name is always an expense account name. - if ($('input[name="destination_account_name"]').length > 0) { - $.getJSON('json/expense-accounts').done(function (data) { - $('input[name="destination_account_name"]').typeahead({source: data}); - }); - } - - // also for multi input - if ($('input[name="destination_account_name[]"]').length > 0) { - $.getJSON('json/expense-accounts').done(function (data) { - $('input[name="destination_account_name[]"]').typeahead({source: data}); - }); - } - - if ($('input[name="tags"]').length > 0) { - $.getJSON('json/tags').done(function (data) { - var opt = { - typeahead: { - source: data - } - }; - $('input[name="tags"]').tagsinput( - opt - ); - }); - } - - // the source account name is always a revenue account name. - if ($('input[name="source_account_name"]').length > 0) { - $.getJSON('json/revenue-accounts').done(function (data) { - $('input[name="source_account_name"]').typeahead({source: data}); - }); - } - // also for multi-input: - if ($('input[name="source_account_name[]"]').length > 0) { - $.getJSON('json/revenue-accounts').done(function (data) { - $('input[name="source_account_name[]"]').typeahead({source: data}); - }); - } - // and for split: - if ($('input[name="journal_source_account_name"]').length > 0) { - $.getJSON('json/revenue-accounts').done(function (data) { - $('input[name="journal_source_account_name"]').typeahead({source: data}); - }); - } - - - if ($('input[name="description"]').length > 0 && what !== undefined) { - $.getJSON('json/transaction-journals/' + what).done(function (data) { - $('input[name="description"]').typeahead({source: data}); - }); - } - // also for multi input: - if ($('input[name="description[]"]').length > 0 && what !== undefined) { - $.getJSON('json/transaction-journals/' + what).done(function (data) { - $('input[name="description[]"]').typeahead({source: data}); - }); - } - // and for the (rare) journal_description: - if ($('input[name="journal_description"]').length > 0 && what !== undefined) { - $.getJSON('json/transaction-journals/' + what).done(function (data) { - $('input[name="journal_description"]').typeahead({source: data}); - }); - } - - if ($('input[name="category"]').length > 0) { - $.getJSON('json/categories').done(function (data) { - $('input[name="category"]').typeahead({source: data}); - }); - } - - // also for multi input: - if ($('input[name^="category["]').length > 0) { - $.getJSON('json/categories').done(function (data) { - $('input[name^="category["]').typeahead({source: data}); - }); - } - - - - -}); \ No newline at end of file diff --git a/public/js/ff/transactions/edit.js b/public/js/ff/transactions/edit.js deleted file mode 100644 index 62a2b4db14..0000000000 --- a/public/js/ff/transactions/edit.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * edit.js - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -/* globals $ */ -$(document).ready(function () { - "use strict"; - // no special JS for edit transaction. -}); diff --git a/public/js/ff/transactions/list.js b/public/js/ff/transactions/list.js index 22b4089ddf..74dc53f24d 100644 --- a/public/js/ff/transactions/list.js +++ b/public/js/ff/transactions/list.js @@ -1,4 +1,14 @@ -/* globals $, edit_selected_txt, delete_selected_txt */ +/* + * list.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: edit_selected_txt, delete_selected_txt */ $(document).ready(function () { "use strict"; @@ -31,7 +41,7 @@ function goToMassEdit() { var checkedArray = getCheckboxes(); // go to specially crafted URL: - window.location.href = 'transactions/mass-edit/' + checkedArray; + window.location.href = 'transactions/mass/edit/' + checkedArray; return false; } @@ -40,7 +50,7 @@ function goToMassDelete() { var checkedArray = getCheckboxes(); // go to specially crafted URL: - window.location.href = 'transactions/mass-delete/' + checkedArray; + window.location.href = 'transactions/mass/delete/' + checkedArray; return false; } diff --git a/public/js/ff/transactions/mass/edit.js b/public/js/ff/transactions/mass/edit.js new file mode 100644 index 0000000000..0c2ec4a3ba --- /dev/null +++ b/public/js/ff/transactions/mass/edit.js @@ -0,0 +1,31 @@ +/* + * edit.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: what */ + +$(document).ready(function () { + "use strict"; + + // destination account names: + if ($('input[name^="destination_account_name["]').length > 0) { + $.getJSON('json/expense-accounts').done(function (data) { + $('input[name^="destination_account_name["]').typeahead({source: data}); + }); + } + + // source account name + if ($('input[name^="source_account_name["]').length > 0) { + $.getJSON('json/revenue-accounts').done(function (data) { + $('input[name^="source_account_name["]').typeahead({source: data}); + }); + } + + $.getJSON('json/categories').done(function (data) { + $('input[name^="category["]').typeahead({source: data}); + }); +}); \ No newline at end of file diff --git a/public/js/ff/transactions/create.js b/public/js/ff/transactions/single/create.js similarity index 65% rename from public/js/ff/transactions/create.js rename to public/js/ff/transactions/single/create.js index f3fc98a09c..7a38946984 100644 --- a/public/js/ff/transactions/create.js +++ b/public/js/ff/transactions/single/create.js @@ -1,26 +1,81 @@ /* * create.js - * Copyright (C) 2016 thegrumpydictator@gmail.com + * 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. * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * See the LICENSE file for details. */ -/* globals what:true, $, doSwitch, txt, middleCrumbName, title,button, middleCrumbUrl, piggiesLength, breadcrumbs */ +/** global: what,Modernizr, title, breadcrumbs, middleCrumbName, button, piggiesLength, txt, doSwitch, middleCrumbUrl */ + $(document).ready(function () { "use strict"; // respond to switch buttons when // creating stuff: - if (doSwitch) { + if (doSwitch == true) { updateButtons(); updateForm(); updateLayout(); + updateDescription(); } + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } + + // get JSON things: + getJSONautocomplete(); }); +function updateDescription() { + $.getJSON('json/transaction-journals/' + what).done(function (data) { + $('input[name="description"]').typeahead('destroy'); + $('input[name="description"]').typeahead({source: data}); + }); +} + +function getJSONautocomplete() { + + // for withdrawals + $.getJSON('json/expense-accounts').done(function (data) { + $('input[name="destination_account_name"]').typeahead({source: data}); + }); + + // for tags: + if ($('input[name="tags"]').length > 0) { + $.getJSON('json/tags').done(function (data) { + + var opt = { + typeahead: { + source: data, + afterSelect: function () { + this.$element.val(""); + } + } + }; + $('input[name="tags"]').tagsinput( + opt + ); + }); + } + + // for deposits + $.getJSON('json/revenue-accounts').done(function (data) { + $('input[name="source_account_name"]').typeahead({source: data}); + }); + + $.getJSON('json/categories').done(function (data) { + $('input[name="category"]').typeahead({source: data}); + }); + +} + function updateLayout() { "use strict"; $('#subTitle').text(title[what]); @@ -94,6 +149,9 @@ function updateForm() { $('#piggy_bank_id_holder').show(); } break; + default: + // no action. + break; } } @@ -110,7 +168,6 @@ function updateButtons() { if (button.data('what') == what) { button.removeClass('btn-default').addClass('btn-info').html(' ' + txt[button.data('what')]); - console.log('Now displaying form for ' + what); } else { button.removeClass('btn-info').addClass('btn-default').text(txt[button.data('what')]); } @@ -126,6 +183,7 @@ function clickButton(e) { updateButtons(); updateForm(); updateLayout(); + updateDescription(); } return false; } \ No newline at end of file diff --git a/public/js/ff/transactions/single/edit.js b/public/js/ff/transactions/single/edit.js new file mode 100644 index 0000000000..a025212d43 --- /dev/null +++ b/public/js/ff/transactions/single/edit.js @@ -0,0 +1,62 @@ +/* + * edit.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: what, Modernizr */ + +$(document).ready(function () { + "use strict"; + // give date a datepicker if not natively supported. + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } + + // the destination account name is always an expense account name. + if ($('input[name="destination_account_name"]').length > 0) { + $.getJSON('json/expense-accounts').done(function (data) { + $('input[name="destination_account_name"]').typeahead({source: data}); + }); + } + + $.getJSON('json/tags').done(function (data) { + + var opt = { + typeahead: { + source: data, + afterSelect: function () { + this.$element.val(""); + } + } + }; + $('input[name="tags"]').tagsinput( + opt + ); + }); + + // the source account name is always a revenue account name. + if ($('input[name="source_account_name"]').length > 0) { + $.getJSON('json/revenue-accounts').done(function (data) { + $('input[name="source_account_name"]').typeahead({source: data}); + }); + } + + $.getJSON('json/transaction-journals/' + what).done(function (data) { + $('input[name="description"]').typeahead({source: data}); + }); + + + $.getJSON('json/categories').done(function (data) { + $('input[name="category"]').typeahead({source: data}); + }); + +}); diff --git a/public/js/ff/split/journal/from-store.js b/public/js/ff/transactions/split/edit.js similarity index 69% rename from public/js/ff/split/journal/from-store.js rename to public/js/ff/transactions/split/edit.js index a6f8a1ef35..19fffbdf4b 100644 --- a/public/js/ff/split/journal/from-store.js +++ b/public/js/ff/transactions/split/edit.js @@ -1,60 +1,84 @@ /* - * from-store.js - * Copyright (C) 2016 thegrumpydictator@gmail.com + * edit.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. + * See the LICENSE file for details. */ -/* globals globalSum */ + +/** global: originalSum, accounting, what, Modernizr */ var destAccounts = {}; var srcAccounts = {}; var categories = {}; -$(function () { +var descriptions = {}; + +$(document).ready(function () { "use strict"; $('.btn-do-split').click(cloneRow); $('.remove-current-split').click(removeRow); $.getJSON('json/expense-accounts').done(function (data) { destAccounts = data; - console.log('destAccounts length is now ' + destAccounts.length); $('input[name$="destination_account_name]"]').typeahead({source: destAccounts}); }); $.getJSON('json/revenue-accounts').done(function (data) { srcAccounts = data; - console.log('srcAccounts length is now ' + srcAccounts.length); $('input[name$="source_account_name]"]').typeahead({source: srcAccounts}); }); $.getJSON('json/categories').done(function (data) { categories = data; - console.log('categories length is now ' + categories.length); $('input[name$="category]"]').typeahead({source: categories}); }); + $.getJSON('json/transaction-journals/' + what).done(function (data) { + descriptions = data; + $('input[name="journal_description"]').typeahead({source: descriptions}); + $('input[name$="description]"]').typeahead({source: descriptions}); + }); + + $.getJSON('json/tags').done(function (data) { + + var opt = { + typeahead: { + source: data, + afterSelect: function () { + this.$element.val(""); + } + } + }; + $('input[name="tags"]').tagsinput( + opt + ); + }); + + $('input[name$="][amount]"]').on('input', calculateSum); - // add auto complete: - - + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } }); + function removeRow(e) { "use strict"; var rows = $('table.split-table tbody tr'); if (rows.length === 1) { - console.log('Will not remove last split'); return false; } var row = $(e.target); var index = row.data('split'); - console.log('Trying to remove row with split ' + index); $('table.split-table tbody tr[data-split="' + index + '"]').remove(); - resetSplits(); return false; @@ -65,31 +89,23 @@ function cloneRow() { "use strict"; var source = $('.table.split-table tbody tr').last().clone(); var count = $('.split-table tbody tr').length + 1; - var index = count - 1; source.removeClass('initial-row'); source.find('.count').text('#' + count); - // // get each input, change the name? - // $.each(source.find('input, select'), function (i, v) { - // var obj = $(v); - // var name = obj.attr('name').replace('[0]', '[' + index + ']'); - // obj.attr('name', name); - // }); - source.find('input[name$="][amount]"]').val("").on('input', calculateSum); if (destAccounts.length > 0) { - console.log('Will be able to extend dest-accounts.'); source.find('input[name$="destination_account_name]"]').typeahead({source: destAccounts}); } if (destAccounts.length > 0) { - console.log('Will be able to extend src-accounts.'); source.find('input[name$="source_account_name]"]').typeahead({source: srcAccounts}); } if (categories.length > 0) { - console.log('Will be able to extend categories.'); source.find('input[name$="category]"]').typeahead({source: categories}); } + if (descriptions.length > 0) { + source.find('input[name$="description]"]').typeahead({source: descriptions}); + } $('.split-table tbody').append(source); @@ -111,7 +127,6 @@ function resetSplits() { $.each($('table.split-table tbody tr'), function (i, v) { var row = $(v); row.attr('data-split', i); - console.log('Row is now ' + row.data('split')); }); // loop each remove button, update the index @@ -119,7 +134,6 @@ function resetSplits() { var button = $(v); button.attr('data-split', i); button.find('i').attr('data-split', i); - console.log('Remove button index is now ' + button.data('split')); }); @@ -128,7 +142,6 @@ function resetSplits() { var cell = $(v); var index = i + 1; cell.text('#' + index); - console.log('Cell is now ' + cell.text()); }); // loop each possible field. @@ -137,37 +150,31 @@ function resetSplits() { $.each($('input[name$="][description]"]'), function (i, v) { var input = $(v); input.attr('name', 'transactions[' + i + '][description]'); - console.log('description is now ' + input.attr('name')); }); // ends with ][destination_account_name] $.each($('input[name$="][destination_account_name]"]'), function (i, v) { var input = $(v); input.attr('name', 'transactions[' + i + '][destination_account_name]'); - console.log('destination_account_name is now ' + input.attr('name')); }); // ends with ][source_account_name] $.each($('input[name$="][source_account_name]"]'), function (i, v) { var input = $(v); - input.attr('name', 'transaction[' + i + '][source_account_name]'); - console.log('source_account_name is now ' + input.attr('name')); + input.attr('name', 'transactions[' + i + '][source_account_name]'); }); // ends with ][amount] $.each($('input[name$="][amount]"]'), function (i, v) { var input = $(v); input.attr('name', 'transactions[' + i + '][amount]'); - console.log('amount is now ' + input.attr('name')); }); // ends with ][budget_id] - $.each($('input[name$="][budget_id]"]'), function (i, v) { + $.each($('select[name$="][budget_id]"]'), function (i, v) { var input = $(v); input.attr('name', 'transactions[' + i + '][budget_id]'); - console.log('budget_id is now ' + input.attr('name')); }); // ends with ][category] $.each($('input[name$="][category]"]'), function (i, v) { var input = $(v); input.attr('name', 'transactions[' + i + '][category]'); - console.log('category is now ' + input.attr('name')); }); } @@ -178,14 +185,13 @@ function calculateSum() { for (var i = 0; i < set.length; i++) { var current = $(set[i]); sum += (current.val() == "" ? 0 : parseFloat(current.val())); - } - console.log("Sum is now " + sum); + sum = Math.round(sum * 100) / 100; + $('.amount-warning').remove(); if (sum != originalSum) { - console.log(sum + ' does not match ' + originalSum); var holder = $('#journal_amount_holder'); var par = holder.find('p.form-control-static'); - var amount = $('').text(' (' + accounting.formatMoney(sum) + ')').addClass('text-danger amount-warning').appendTo(par); + $('').text(' (' + accounting.formatMoney(sum) + ')').addClass('text-danger amount-warning').appendTo(par); } } \ No newline at end of file diff --git a/public/js/lib/Chart.bundle.min.js b/public/js/lib/Chart.bundle.min.js index 4acd46a8da..792c8e14b5 100644 --- a/public/js/lib/Chart.bundle.min.js +++ b/public/js/lib/Chart.bundle.min.js @@ -1,15 +1,15 @@ /*! * Chart.js * http://chartjs.org/ - * Version: 2.2.2 + * Version: 2.3.0 * * Copyright 2016 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 e(t,n,i){function a(r,s){if(!n[r]){if(!t[r]){var l="function"==typeof require&&require;if(!s&&l)return l(r,!0);if(o)return o(r,!0);var d=new Error("Cannot find module '"+r+"'");throw d.code="MODULE_NOT_FOUND",d}var u=n[r]={exports:{}};t[r][0].call(u.exports,function(e){var n=t[r][1][e];return a(n?n:e)},u,u.exports,e,t,n,i)}return n[r].exports}for(var o="function"==typeof require&&require,r=0;re||t[3]&&t[3]<1?c(t,e):"rgb("+t[0]+", "+t[1]+", "+t[2]+")"}function c(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"rgba("+t[0]+", "+t[1]+", "+t[2]+", "+e+")"}function h(t,e){if(1>e||t[3]&&t[3]<1)return f(t,e);var n=Math.round(t[0]/255*100),i=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgb("+n+"%, "+i+"%, "+a+"%)"}function f(t,e){var n=Math.round(t[0]/255*100),i=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgba("+n+"%, "+i+"%, "+a+"%, "+(e||t[3]||1)+")"}function g(t,e){return 1>e||t[3]&&t[3]<1?m(t,e):"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"}function m(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"hsla("+t[0]+", "+t[1]+"%, "+t[2]+"%, "+e+")"}function p(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"}function v(t){return k[t.slice(0,3)]}function b(t,e,n){return Math.min(Math.max(e,t),n)}function y(t){var e=t.toString(16).toUpperCase();return e.length<2?"0"+e:e}var x=t(5);e.exports={getRgba:i,getHsla:a,getRgb:r,getHsl:s,getHwb:o,getAlpha:l,hexString:d,rgbString:u,rgbaString:c,percentString:h,percentaString:f,hslString:g,hslaString:m,hwbString:p,keyword:v};var k={};for(var S in x)k[x[S]]=S},{5:5}],2:[function(t,e,n){var i=t(4),a=t(1),o=function(t){if(t instanceof o)return t;if(!(this instanceof o))return new o(t);this.values={rgb:[0,0,0],hsl:[0,0,0],hsv:[0,0,0],hwb:[0,0,0],cmyk:[0,0,0,0],alpha:1};var e;if("string"==typeof t)if(e=a.getRgba(t))this.setValues("rgb",e);else if(e=a.getHsla(t))this.setValues("hsl",e);else{if(!(e=a.getHwb(t)))throw new Error('Unable to parse color from string "'+t+'"');this.setValues("hwb",e)}else if("object"==typeof t)if(e=t,void 0!==e.r||void 0!==e.red)this.setValues("rgb",e);else if(void 0!==e.l||void 0!==e.lightness)this.setValues("hsl",e);else if(void 0!==e.v||void 0!==e.value)this.setValues("hsv",e);else if(void 0!==e.w||void 0!==e.whiteness)this.setValues("hwb",e);else{if(void 0===e.c&&void 0===e.cyan)throw new Error("Unable to parse color from object "+JSON.stringify(t));this.setValues("cmyk",e)}};o.prototype={rgb:function(){return this.setSpace("rgb",arguments)},hsl:function(){return this.setSpace("hsl",arguments)},hsv:function(){return this.setSpace("hsv",arguments)},hwb:function(){return this.setSpace("hwb",arguments)},cmyk:function(){return this.setSpace("cmyk",arguments)},rgbArray:function(){return this.values.rgb},hslArray:function(){return this.values.hsl},hsvArray:function(){return this.values.hsv},hwbArray:function(){var t=this.values;return 1!==t.alpha?t.hwb.concat([t.alpha]):t.hwb},cmykArray:function(){return this.values.cmyk},rgbaArray:function(){var t=this.values;return t.rgb.concat([t.alpha])},hslaArray:function(){var t=this.values;return t.hsl.concat([t.alpha])},alpha:function(t){return void 0===t?this.values.alpha:(this.setValues("alpha",t),this)},red:function(t){return this.setChannel("rgb",0,t)},green:function(t){return this.setChannel("rgb",1,t)},blue:function(t){return this.setChannel("rgb",2,t)},hue:function(t){return t&&(t%=360,t=0>t?360+t:t),this.setChannel("hsl",0,t)},saturation:function(t){return this.setChannel("hsl",1,t)},lightness:function(t){return this.setChannel("hsl",2,t)},saturationv:function(t){return this.setChannel("hsv",1,t)},whiteness:function(t){return this.setChannel("hwb",1,t)},blackness:function(t){return this.setChannel("hwb",2,t)},value:function(t){return this.setChannel("hsv",2,t)},cyan:function(t){return this.setChannel("cmyk",0,t)},magenta:function(t){return this.setChannel("cmyk",1,t)},yellow:function(t){return this.setChannel("cmyk",2,t)},black:function(t){return this.setChannel("cmyk",3,t)},hexString:function(){return a.hexString(this.values.rgb)},rgbString:function(){return a.rgbString(this.values.rgb,this.values.alpha)},rgbaString:function(){return a.rgbaString(this.values.rgb,this.values.alpha)},percentString:function(){return a.percentString(this.values.rgb,this.values.alpha)},hslString:function(){return a.hslString(this.values.hsl,this.values.alpha)},hslaString:function(){return a.hslaString(this.values.hsl,this.values.alpha)},hwbString:function(){return a.hwbString(this.values.hwb,this.values.alpha)},keyword:function(){return a.keyword(this.values.rgb,this.values.alpha)},rgbNumber:function(){var t=this.values.rgb;return t[0]<<16|t[1]<<8|t[2]},luminosity:function(){for(var t=this.values.rgb,e=[],n=0;n=i?i/12.92:Math.pow((i+.055)/1.055,2.4)}return.2126*e[0]+.7152*e[1]+.0722*e[2]},contrast:function(t){var e=this.luminosity(),n=t.luminosity();return e>n?(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 128>e},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;3>e;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]=0>n?360+n:n,this.setValues("hsl",e),this},mix:function(t,e){var n=this,i=t,a=void 0===e?.5:e,o=2*a-1,r=n.alpha()-i.alpha(),s=((o*r===-1?o:(o+r)/(1+o*r))+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 o,i=this.values,a=n.values;for(var r in i)i.hasOwnProperty(r)&&(t=i[r],e={}.toString.call(t),"[object Array]"===e?a[r]=t.slice(0):"[object Number]"===e?a[r]=t:console.error("unexpected color value:",t));return n}},o.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},o.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},o.prototype.getValues=function(t){for(var e=this.values,n={},i=0;ie&&(e+=360),i=(s+l)/2,n=l==s?0:.5>=i?d/(l+s):d/(2-l-s),[e,100*n,100*i]}function a(t){var e,n,i,a=t[0],o=t[1],r=t[2],s=Math.min(a,o,r),l=Math.max(a,o,r),d=l-s;return n=0==l?0:d/l*1e3/10,l==s?e=0:a==l?e=(o-r)/d:o==l?e=2+(r-a)/d:r==l&&(e=4+(a-o)/d),e=Math.min(60*e,360),0>e&&(e+=360),i=l/255*1e3/10,[e,n,i]}function o(t){var e=t[0],n=t[1],a=t[2],o=i(t)[0],r=1/255*Math.min(e,Math.min(n,a)),a=1-1/255*Math.max(e,Math.max(n,a));return[o,100*r,100*a]}function s(t){var e,n,i,a,o=t[0]/255,r=t[1]/255,s=t[2]/255;return a=Math.min(1-o,1-r,1-s),e=(1-o-a)/(1-a)||0,n=(1-r-a)/(1-a)||0,i=(1-s-a)/(1-a)||0,[100*e,100*n,100*i,100*a]}function l(t){return K[JSON.stringify(t)]}function d(t){var e=t[0]/255,n=t[1]/255,i=t[2]/255;e=e>.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,o=.2126*e+.7152*n+.0722*i,r=.0193*e+.1192*n+.9505*i;return[100*a,100*o,100*r]}function u(t){var e,n,i,a=d(t),o=a[0],r=a[1],s=a[2];return o/=95.047,r/=100,s/=108.883,o=o>.008856?Math.pow(o,1/3):7.787*o+16/116,r=r>.008856?Math.pow(r,1/3):7.787*r+16/116,s=s>.008856?Math.pow(s,1/3):7.787*s+16/116,e=116*r-16,n=500*(o-r),i=200*(r-s),[e,n,i]}function c(t){return B(u(t))}function h(t){var e,n,i,a,o,r=t[0]/360,s=t[1]/100,l=t[2]/100;if(0==s)return o=255*l,[o,o,o];n=.5>l?l*(1+s):l+s-l*s,e=2*l-n,a=[0,0,0];for(var d=0;3>d;d++)i=r+1/3*-(d-1),0>i&&i++,i>1&&i--,o=1>6*i?e+6*(n-e)*i:1>2*i?n:2>3*i?e+(n-e)*(2/3-i)*6:e,a[d]=255*o;return a}function f(t){var e,n,i=t[0],a=t[1]/100,o=t[2]/100;return 0===o?[0,0,0]:(o*=2,a*=1>=o?o:2-o,n=(o+a)/2,e=2*a/(o+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,o=e-Math.floor(e),r=255*i*(1-n),s=255*i*(1-n*o),l=255*i*(1-n*(1-o)),i=255*i;switch(a){case 0:return[i,l,r];case 1:return[s,i,r];case 2:return[r,i,l];case 3:return[r,s,i];case 4:return[l,r,i];case 5:return[i,r,s]}}function x(t){var e,n,i=t[0],a=t[1]/100,o=t[2]/100;return n=(2-a)*o,e=a*o,e/=1>=n?n:2-n,e=e||0,n/=2,[i,100*e,100*n]}function k(t){return o(y(t))}function S(t){return s(y(t))}function w(t){return l(y(t))}function _(t){var e,n,i,a,o=t[0]/360,s=t[1]/100,l=t[2]/100,d=s+l;switch(d>1&&(s/=d,l/=d),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(_(t))}function D(t){return a(_(t))}function C(t){return s(_(t))}function T(t){return l(_(t))}function P(t){var e,n,i,a=t[0]/100,o=t[1]/100,r=t[2]/100,s=t[3]/100;return e=1-Math.min(1,a*(1-s)+s),n=1-Math.min(1,o*(1-s)+s),i=1-Math.min(1,r*(1-s)+s),[255*e,255*n,255*i]}function A(t){return i(P(t))}function F(t){return a(P(t))}function I(t){return o(P(t))}function O(t){return l(P(t))}function R(t){var e,n,i,a=t[0]/100,o=t[1]/100,r=t[2]/100;return e=3.2406*a+-1.5372*o+r*-.4986,n=a*-.9689+1.8758*o+.0415*r,i=.0557*a+o*-.204+1.057*r,e=e>.0031308?1.055*Math.pow(e,1/2.4)-.055:e=12.92*e,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:n=12.92*n,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:i=12.92*i,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 W(t){var e,n,i,a=t[0],o=t[1],r=t[2];return a/=95.047,o/=100,r/=108.883,a=a>.008856?Math.pow(a,1/3):7.787*a+16/116,o=o>.008856?Math.pow(o,1/3):7.787*o+16/116,r=r>.008856?Math.pow(r,1/3):7.787*r+16/116,e=116*o-16,n=500*(a-o),i=200*(o-r),[e,n,i]}function L(t){return B(W(t))}function V(t){var e,n,i,a,o=t[0],r=t[1],s=t[2];return 8>=o?(n=100*o/903.3,a=7.787*(n/100)+16/116):(n=100*Math.pow((o+16)/116,3),a=Math.pow(n/100,1/3)),e=.008856>=e/95.047?e=95.047*(r/500+a-16/116)/7.787:95.047*Math.pow(r/500+a,3),i=.008859>=i/108.883?i=108.883*(a-s/200-16/116)/7.787:108.883*Math.pow(a-s/200,3),[e,n,i]}function B(t){var e,n,i,a=t[0],o=t[1],r=t[2];return e=Math.atan2(r,o),n=360*e/2/Math.PI,0>n&&(n+=360),i=Math.sqrt(o*o+r*r),[a,i,n]}function Y(t){return R(V(t))}function z(t){var e,n,i,a=t[0],o=t[1],r=t[2];return i=r/360*2*Math.PI,e=o*Math.cos(i),n=o*Math.sin(i),[a,e,n]}function N(t){return V(z(t))}function H(t){return Y(z(t))}function E(t){return X[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 u(E(t))}function J(t){return d(E(t))}e.exports={rgb2hsl:i,rgb2hsv:a,rgb2hwb:o,rgb2cmyk:s,rgb2keyword:l,rgb2xyz:d,rgb2lab:u,rgb2lch:c,hsl2rgb:h,hsl2hsv:f,hsl2hwb:m,hsl2cmyk:p,hsl2keyword:v,hsv2rgb:y,hsv2hsl:x,hsv2hwb:k,hsv2cmyk:S,hsv2keyword:w,hwb2rgb:_,hwb2hsl:M,hwb2hsv:D,hwb2cmyk:C,hwb2keyword:T,cmyk2rgb:P,cmyk2hsl:A,cmyk2hsv:F,cmyk2hwb:I,cmyk2keyword:O,keyword2rgb:E,keyword2hsl:U,keyword2hsv:j,keyword2hwb:G,keyword2cmyk:q,keyword2lab:Z,keyword2xyz:J,xyz2rgb:R,xyz2lab:W,xyz2lch:L,lab2xyz:V,lab2rgb:Y,lab2lch:B,lch2lab:z,lch2xyz:N,lch2rgb:H};var X={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 X)K[JSON.stringify(X[Q])]=Q},{}],4:[function(t,e,n){var i=t(3),a=function(){return new d};for(var o in i){a[o+"Raw"]=function(t){return function(e){return"number"==typeof e&&(e=Array.prototype.slice.call(arguments)),i[t](e)}}(o);var r=/(\w+)2(\w+)/.exec(o),s=r[1],l=r[2];a[s]=a[s]||{},a[s][l]=a[o]=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 vi)i=vi[n],a=e[i],p(a)||(t[i]=a);return t}function b(e){v(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),bi===!1&&(bi=!0,t.updateOffset(this),bi=!1)}function y(t){return t instanceof b||null!=t&&null!=t._isAMomentObject}function x(t){return 0>t?Math.ceil(t)||0:Math.floor(t)}function k(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=x(e)),n}function S(t,e,n){var i,a=Math.min(t.length,e.length),o=Math.abs(t.length-e.length),r=0;for(i=0;a>i;i++)(n&&t[i]!==e[i]||!n&&k(t[i])!==k(e[i]))&&r++;return r+o}function w(e){t.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+e)}function _(e,n){var i=!0;return u(function(){return null!=t.deprecationHandler&&t.deprecationHandler(null,e),i&&(w(e+"\nArguments: "+Array.prototype.slice.call(arguments).join(", ")+"\n"+(new Error).stack),i=!1),n.apply(this,arguments)},n)}function M(e,n){null!=t.deprecationHandler&&t.deprecationHandler(e,n),yi[e]||(w(n),yi[e]=!0)}function D(t){return t instanceof Function||"[object Function]"===Object.prototype.toString.call(t)}function C(t){var e,n;for(n in t)e=t[n],D(e)?this[n]=e:this["_"+n]=e;this._config=t,this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function T(t,e){var n,i=u({},t);for(n in e)d(e,n)&&(o(t[n])&&o(e[n])?(i[n]={},u(i[n],t[n]),u(i[n],e[n])):null!=e[n]?i[n]=e[n]:delete i[n]);for(n in t)d(t,n)&&!d(e,n)&&o(t[n])&&(i[n]=u({},i[n]));return i}function P(t){null!=t&&this.set(t)}function A(t,e,n){var i=this._calendar[t]||this._calendar.sameElse;return D(i)?i.call(e,n):i}function F(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t])}function I(){return this._invalidDate}function O(t){return this._ordinal.replace("%d",t)}function R(t,e,n,i){var a=this._relativeTime[n];return D(a)?a(t,e,n,i):a.replace(/%d/i,t)}function W(t,e){var n=this._relativeTime[t>0?"future":"past"];return D(n)?n(e):n.replace(/%s/i,e)}function L(t,e){var n=t.toLowerCase();Ti[n]=Ti[n+"s"]=Ti[e]=t}function V(t){return"string"==typeof t?Ti[t]||Ti[t.toLowerCase()]:void 0}function B(t){var e,n,i={};for(n in t)d(t,n)&&(e=V(n),e&&(i[e]=t[n]));return i}function Y(t,e){Pi[t]=e}function z(t){var e=[];for(var n in t)e.push({unit:n,priority:Pi[n]});return e.sort(function(t,e){return t.priority-e.priority}),e}function N(e,n){return function(i){return null!=i?(E(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 E(t,e,n){t.isValid()&&t._d["set"+(t._isUTC?"UTC":"")+e](n)}function U(t){return t=V(t),D(this[t])?this[t]():this}function j(t,e){if("object"==typeof t){t=B(t);for(var n=z(t),i=0;i=0;return(o?n?"+":"":"-")+Math.pow(10,Math.max(0,a)).toString().substr(1)+i}function q(t,e,n,i){var a=i;"string"==typeof i&&(a=function(){return this[i]()}),t&&(Oi[t]=a),e&&(Oi[e[0]]=function(){return G(a.apply(this,arguments),e[1],e[2])}),n&&(Oi[n]=function(){return this.localeData().ordinal(a.apply(this,arguments),t)})}function Z(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function J(t){var e,n,i=t.match(Ai);for(e=0,n=i.length;n>e;e++)Oi[i[e]]?i[e]=Oi[i[e]]:i[e]=Z(i[e]);return function(e){var a,o="";for(a=0;n>a;a++)o+=i[a]instanceof Function?i[a].call(e,t):i[a];return o}}function X(t,e){return t.isValid()?(e=K(e,t.localeData()),Ii[e]=Ii[e]||J(e),Ii[e](t)):t.localeData().invalidDate()}function K(t,e){function n(t){return e.longDateFormat(t)||t}var i=5;for(Fi.lastIndex=0;i>=0&&Fi.test(t);)t=t.replace(Fi,n),Fi.lastIndex=0,i-=1;return t}function Q(t,e,n){Ki[t]=D(e)?e:function(t,i){return t&&n?n:e}}function $(t,e){return d(Ki,t)?Ki[t](e._strict,e._locale):new RegExp(tt(t))}function tt(t){return et(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,n,i,a){return e||n||i||a}))}function et(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function nt(t,e){var n,i=e;for("string"==typeof t&&(t=[t]),"number"==typeof e&&(i=function(t,n){n[e]=k(t)}),n=0;ni;++i)o=c([2e3,i]),this._shortMonthsParse[i]=this.monthsShort(o,"").toLocaleLowerCase(),this._longMonthsParse[i]=this.months(o,"").toLocaleLowerCase();return n?"MMM"===e?(a=ki.call(this._shortMonthsParse,r),-1!==a?a:null):(a=ki.call(this._longMonthsParse,r),-1!==a?a:null):"MMM"===e?(a=ki.call(this._shortMonthsParse,r),-1!==a?a:(a=ki.call(this._longMonthsParse,r),-1!==a?a:null)):(a=ki.call(this._longMonthsParse,r),-1!==a?a:(a=ki.call(this._shortMonthsParse,r),-1!==a?a:null))}function dt(t,e,n){var i,a,o;if(this._monthsParseExact)return lt.call(this,t,e,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),i=0;12>i;i++){if(a=c([2e3,i]),n&&!this._longMonthsParse[i]&&(this._longMonthsParse[i]=new RegExp("^"+this.months(a,"").replace(".","")+"$","i"),this._shortMonthsParse[i]=new RegExp("^"+this.monthsShort(a,"").replace(".","")+"$","i")),n||this._monthsParse[i]||(o="^"+this.months(a,"")+"|^"+this.monthsShort(a,""),this._monthsParse[i]=new RegExp(o.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[i].test(t))return i;if(n&&"MMM"===e&&this._shortMonthsParse[i].test(t))return i;if(!n&&this._monthsParse[i].test(t))return i}}function ut(t,e){var n;if(!t.isValid())return t;if("string"==typeof e)if(/^\d+$/.test(e))e=k(e);else if(e=t.localeData().monthsParse(e),"number"!=typeof e)return t;return n=Math.min(t.date(),ot(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,n),t}function ct(e){return null!=e?(ut(this,e),t.updateOffset(this,!0),this):H(this,"Month")}function ht(){return ot(this.year(),this.month())}function ft(t){return this._monthsParseExact?(d(this,"_monthsRegex")||mt.call(this),t?this._monthsShortStrictRegex:this._monthsShortRegex):(d(this,"_monthsShortRegex")||(this._monthsShortRegex=ca), -this._monthsShortStrictRegex&&t?this._monthsShortStrictRegex:this._monthsShortRegex)}function gt(t){return this._monthsParseExact?(d(this,"_monthsRegex")||mt.call(this),t?this._monthsStrictRegex:this._monthsRegex):(d(this,"_monthsRegex")||(this._monthsRegex=ha),this._monthsStrictRegex&&t?this._monthsStrictRegex:this._monthsRegex)}function mt(){function t(t,e){return e.length-t.length}var e,n,i=[],a=[],o=[];for(e=0;12>e;e++)n=c([2e3,e]),i.push(this.monthsShort(n,"")),a.push(this.months(n,"")),o.push(this.months(n,"")),o.push(this.monthsShort(n,""));for(i.sort(t),a.sort(t),o.sort(t),e=0;12>e;e++)i[e]=et(i[e]),a[e]=et(a[e]);for(e=0;24>e;e++)o[e]=et(o[e]);this._monthsRegex=new RegExp("^("+o.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+i.join("|")+")","i")}function pt(t){return vt(t)?366:365}function vt(t){return t%4===0&&t%100!==0||t%400===0}function bt(){return vt(this.year())}function yt(t,e,n,i,a,o,r){var s=new Date(t,e,n,i,a,o,r);return 100>t&&t>=0&&isFinite(s.getFullYear())&&s.setFullYear(t),s}function xt(t){var e=new Date(Date.UTC.apply(null,arguments));return 100>t&&t>=0&&isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t),e}function kt(t,e,n){var i=7+e-n,a=(7+xt(t,0,i).getUTCDay()-e)%7;return-a+i-1}function St(t,e,n,i,a){var o,r,s=(7+n-i)%7,l=kt(t,i,a),d=1+7*(e-1)+s+l;return 0>=d?(o=t-1,r=pt(o)+d):d>pt(t)?(o=t+1,r=d-pt(t)):(o=t,r=d),{year:o,dayOfYear:r}}function wt(t,e,n){var i,a,o=kt(t.year(),e,n),r=Math.floor((t.dayOfYear()-o-1)/7)+1;return 1>r?(a=t.year()-1,i=r+_t(a,e,n)):r>_t(t.year(),e,n)?(i=r-_t(t.year(),e,n),a=t.year()+1):(a=t.year(),i=r),{week:i,year:a}}function _t(t,e,n){var i=kt(t,e,n),a=kt(t+1,e,n);return(pt(t)-i+a)/7}function Mt(t){return wt(t,this._week.dow,this._week.doy).week}function Dt(){return this._week.dow}function Ct(){return this._week.doy}function Tt(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function Pt(t){var e=wt(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 It(t,e){return a(this._weekdays)?this._weekdays[t.day()]:this._weekdays[this._weekdays.isFormat.test(e)?"format":"standalone"][t.day()]}function Ot(t){return this._weekdaysShort[t.day()]}function Rt(t){return this._weekdaysMin[t.day()]}function Wt(t,e,n){var i,a,o,r=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],i=0;7>i;++i)o=c([2e3,1]).day(i),this._minWeekdaysParse[i]=this.weekdaysMin(o,"").toLocaleLowerCase(),this._shortWeekdaysParse[i]=this.weekdaysShort(o,"").toLocaleLowerCase(),this._weekdaysParse[i]=this.weekdays(o,"").toLocaleLowerCase();return n?"dddd"===e?(a=ki.call(this._weekdaysParse,r),-1!==a?a:null):"ddd"===e?(a=ki.call(this._shortWeekdaysParse,r),-1!==a?a:null):(a=ki.call(this._minWeekdaysParse,r),-1!==a?a:null):"dddd"===e?(a=ki.call(this._weekdaysParse,r),-1!==a?a:(a=ki.call(this._shortWeekdaysParse,r),-1!==a?a:(a=ki.call(this._minWeekdaysParse,r),-1!==a?a:null))):"ddd"===e?(a=ki.call(this._shortWeekdaysParse,r),-1!==a?a:(a=ki.call(this._weekdaysParse,r),-1!==a?a:(a=ki.call(this._minWeekdaysParse,r),-1!==a?a:null))):(a=ki.call(this._minWeekdaysParse,r),-1!==a?a:(a=ki.call(this._weekdaysParse,r),-1!==a?a:(a=ki.call(this._shortWeekdaysParse,r),-1!==a?a:null)))}function Lt(t,e,n){var i,a,o;if(this._weekdaysParseExact)return Wt.call(this,t,e,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),i=0;7>i;i++){if(a=c([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]||(o="^"+this.weekdays(a,"")+"|^"+this.weekdaysShort(a,"")+"|^"+this.weekdaysMin(a,""),this._weekdaysParse[i]=new RegExp(o.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 Vt(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 Yt(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 zt(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Et.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(d(this,"_weekdaysRegex")||(this._weekdaysRegex=ba),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)}function Nt(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Et.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(d(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ya),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Ht(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Et.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(d(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=xa),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Et(){function t(t,e){return e.length-t.length}var e,n,i,a,o,r=[],s=[],l=[],d=[];for(e=0;7>e;e++)n=c([2e3,1]).day(e),i=this.weekdaysMin(n,""),a=this.weekdaysShort(n,""),o=this.weekdays(n,""),r.push(i),s.push(a),l.push(o),d.push(i),d.push(a),d.push(o);for(r.sort(t),s.sort(t),l.sort(t),d.sort(t),e=0;7>e;e++)s[e]=et(s[e]),l[e]=et(l[e]),d[e]=et(d[e]);this._weekdaysRegex=new RegExp("^("+d.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("^("+r.join("|")+")","i")}function Ut(){return this.hours()%12||12}function jt(){return this.hours()||24}function Gt(t,e){q(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function qt(t,e){return e._meridiemParse}function Zt(t){return"p"===(t+"").toLowerCase().charAt(0)}function Jt(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"}function Xt(t){return t?t.toLowerCase().replace("_","-"):t}function Kt(t){for(var e,n,i,a,o=0;o0;){if(i=Qt(a.slice(0,e).join("-")))return i;if(n&&n.length>=e&&S(a,n,!0)>=e-1)break;e--}o++}return null}function Qt(t){var i=null;if(!Ma[t]&&"undefined"!=typeof n&&n&&n.exports)try{i=ka._abbr,e("./locale/"+t),$t(i)}catch(a){}return Ma[t]}function $t(t,e){var n;return t&&(n=p(e)?ne(t):te(t,e),n&&(ka=n)),ka._abbr}function te(t,e){if(null!==e){var n=_a;return e.abbr=t,null!=Ma[t]?(M("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=Ma[t]._config):null!=e.parentLocale&&(null!=Ma[e.parentLocale]?n=Ma[e.parentLocale]._config:M("parentLocaleUndefined","specified parentLocale is not defined yet. See http://momentjs.com/guides/#/warnings/parent-locale/")),Ma[t]=new P(T(n,e)),$t(t),Ma[t]}return delete Ma[t],null}function ee(t,e){if(null!=e){var n,i=_a;null!=Ma[t]&&(i=Ma[t]._config),e=T(i,e),n=new P(e),n.parentLocale=Ma[t],Ma[t]=n,$t(t)}else null!=Ma[t]&&(null!=Ma[t].parentLocale?Ma[t]=Ma[t].parentLocale:null!=Ma[t]&&delete Ma[t]);return Ma[t]}function ne(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return ka;if(!a(t)){if(e=Qt(t))return e;t=[t]}return Kt(t)}function ie(){return xi(Ma)}function ae(t){var e,n=t._a;return n&&-2===f(t).overflow&&(e=n[ta]<0||n[ta]>11?ta:n[ea]<1||n[ea]>ot(n[$i],n[ta])?ea:n[na]<0||n[na]>24||24===n[na]&&(0!==n[ia]||0!==n[aa]||0!==n[oa])?na:n[ia]<0||n[ia]>59?ia:n[aa]<0||n[aa]>59?aa:n[oa]<0||n[oa]>999?oa:-1,f(t)._overflowDayOfYear&&($i>e||e>ea)&&(e=ea),f(t)._overflowWeeks&&-1===e&&(e=ra),f(t)._overflowWeekday&&-1===e&&(e=sa),f(t).overflow=e),t}function oe(t){var e,n,i,a,o,r,s=t._i,l=Da.exec(s)||Ca.exec(s);if(l){for(f(t).iso=!0,e=0,n=Pa.length;n>e;e++)if(Pa[e][1].exec(l[1])){a=Pa[e][0],i=Pa[e][2]!==!1;break}if(null==a)return void(t._isValid=!1);if(l[3]){for(e=0,n=Aa.length;n>e;e++)if(Aa[e][1].exec(l[3])){o=(l[2]||" ")+Aa[e][0];break}if(null==o)return void(t._isValid=!1)}if(!i&&null!=o)return void(t._isValid=!1);if(l[4]){if(!Ta.exec(l[4]))return void(t._isValid=!1);r="Z"}t._f=a+(o||"")+(r||""),ce(t)}else t._isValid=!1}function re(e){var n=Fa.exec(e._i);return null!==n?void(e._d=new Date(+n[1])):(oe(e),void(e._isValid===!1&&(delete e._isValid,t.createFromInputFallback(e))))}function se(t,e,n){return null!=t?t:null!=e?e:n}function le(e){var n=new Date(t.now());return e._useUTC?[n.getUTCFullYear(),n.getUTCMonth(),n.getUTCDate()]:[n.getFullYear(),n.getMonth(),n.getDate()]}function de(t){var e,n,i,a,o=[];if(!t._d){for(i=le(t),t._w&&null==t._a[ea]&&null==t._a[ta]&&ue(t),t._dayOfYear&&(a=se(t._a[$i],i[$i]),t._dayOfYear>pt(a)&&(f(t)._overflowDayOfYear=!0),n=xt(a,0,t._dayOfYear),t._a[ta]=n.getUTCMonth(),t._a[ea]=n.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=o[e]=i[e];for(;7>e;e++)t._a[e]=o[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[na]&&0===t._a[ia]&&0===t._a[aa]&&0===t._a[oa]&&(t._nextDay=!0,t._a[na]=0),t._d=(t._useUTC?xt:yt).apply(null,o),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[na]=24)}}function ue(t){var e,n,i,a,o,r,s,l;e=t._w,null!=e.GG||null!=e.W||null!=e.E?(o=1,r=4,n=se(e.GG,t._a[$i],wt(ye(),1,4).year),i=se(e.W,1),a=se(e.E,1),(1>a||a>7)&&(l=!0)):(o=t._locale._week.dow,r=t._locale._week.doy,n=se(e.gg,t._a[$i],wt(ye(),o,r).year),i=se(e.w,1),null!=e.d?(a=e.d,(0>a||a>6)&&(l=!0)):null!=e.e?(a=e.e+o,(e.e<0||e.e>6)&&(l=!0)):a=o),1>i||i>_t(n,o,r)?f(t)._overflowWeeks=!0:null!=l?f(t)._overflowWeekday=!0:(s=St(n,i,a,o,r),t._a[$i]=s.year,t._dayOfYear=s.dayOfYear)}function ce(e){if(e._f===t.ISO_8601)return void oe(e);e._a=[],f(e).empty=!0;var n,i,a,o,r,s=""+e._i,l=s.length,d=0;for(a=K(e._f,e._locale).match(Ai)||[],n=0;n0&&f(e).unusedInput.push(r),s=s.slice(s.indexOf(i)+i.length),d+=i.length),Oi[o]?(i?f(e).empty=!1:f(e).unusedTokens.push(o),at(o,i,e)):e._strict&&!i&&f(e).unusedTokens.push(o);f(e).charsLeftOver=l-d,s.length>0&&f(e).unusedInput.push(s),e._a[na]<=12&&f(e).bigHour===!0&&e._a[na]>0&&(f(e).bigHour=void 0),f(e).parsedDateParts=e._a.slice(0),f(e).meridiem=e._meridiem,e._a[na]=he(e._locale,e._a[na],e._meridiem),de(e),ae(e)}function he(t,e,n){var i;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?(i=t.isPM(n),i&&12>e&&(e+=12),i||12!==e||(e=0),e):e}function fe(t){var e,n,i,a,o;if(0===t._f.length)return f(t).invalidFormat=!0,void(t._d=new Date(NaN));for(a=0;ao)&&(i=o,n=e));u(t,n||e)}function ge(t){if(!t._d){var e=B(t._i);t._a=l([e.year,e.month,e.day||e.date,e.hour,e.minute,e.second,e.millisecond],function(t){return t&&parseInt(t,10)}),de(t)}}function me(t){var e=new b(ae(pe(t)));return e._nextDay&&(e.add(1,"d"),e._nextDay=void 0),e}function pe(t){var e=t._i,n=t._f;return t._locale=t._locale||ne(t._l),null===e||void 0===n&&""===e?m({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),y(e)?new b(ae(e)):(a(n)?fe(t):s(e)?t._d=e:n?ce(t):ve(t),g(t)||(t._d=null),t))}function ve(e){var n=e._i;void 0===n?e._d=new Date(t.now()):s(n)?e._d=new Date(n.valueOf()):"string"==typeof n?re(e):a(n)?(e._a=l(n.slice(0),function(t){return parseInt(t,10)}),de(e)):"object"==typeof n?ge(e):"number"==typeof n?e._d=new Date(n):t.createFromInputFallback(e)}function be(t,e,n,i,s){var l={};return"boolean"==typeof n&&(i=n,n=void 0),(o(t)&&r(t)||a(t)&&0===t.length)&&(t=void 0),l._isAMomentObject=!0,l._useUTC=l._isUTC=s,l._l=n,l._i=t,l._f=e,l._strict=i,me(l)}function ye(t,e,n,i){return be(t,e,n,i,!1)}function xe(t,e){var n,i;if(1===e.length&&a(e[0])&&(e=e[0]),!e.length)return ye();for(n=e[0],i=1;it&&(t=-t,n="-"),n+G(~~(t/60),2)+e+G(~~t%60,2)})}function De(t,e){var n=(e||"").match(t)||[],i=n[n.length-1]||[],a=(i+"").match(Wa)||["-",0,0],o=+(60*a[1])+k(a[2]);return"+"===a[0]?o:-o}function Ce(e,n){var i,a;return n._isUTC?(i=n.clone(),a=(y(e)||s(e)?e.valueOf():ye(e).valueOf())-i.valueOf(),i._d.setTime(i._d.valueOf()+a),t.updateOffset(i,!1),i):ye(e).local()}function Te(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function Pe(e,n){var i,a=this._offset||0;return this.isValid()?null!=e?("string"==typeof e?e=De(Zi,e):Math.abs(e)<16&&(e=60*e),!this._isUTC&&n&&(i=Te(this)),this._offset=e,this._isUTC=!0,null!=i&&this.add(i,"m"),a!==e&&(!n||this._changeInProgress?Ge(this,ze(e-a,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,t.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?a:Te(this):null!=e?this:NaN}function Ae(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()}function Fe(t){return this.utcOffset(0,t)}function Ie(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Te(this),"m")),this}function Oe(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(De(qi,this._i)),this}function Re(t){return this.isValid()?(t=t?ye(t).utcOffset():0,(this.utcOffset()-t)%60===0):!1}function We(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Le(){if(!p(this._isDSTShifted))return this._isDSTShifted;var t={};if(v(t,this),t=pe(t),t._a){var e=t._isUTC?c(t._a):ye(t._a);this._isDSTShifted=this.isValid()&&S(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Ve(){return this.isValid()?!this._isUTC:!1}function Be(){return this.isValid()?this._isUTC:!1}function Ye(){return this.isValid()?this._isUTC&&0===this._offset:!1}function ze(t,e){var n,i,a,o=t,r=null;return _e(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(r=La.exec(t))?(n="-"===r[1]?-1:1,o={y:0,d:k(r[ea])*n,h:k(r[na])*n,m:k(r[ia])*n,s:k(r[aa])*n,ms:k(r[oa])*n}):(r=Va.exec(t))?(n="-"===r[1]?-1:1,o={y:Ne(r[2],n),M:Ne(r[3],n),w:Ne(r[4],n),d:Ne(r[5],n),h:Ne(r[6],n),m:Ne(r[7],n),s:Ne(r[8],n)}):null==o?o={}:"object"==typeof o&&("from"in o||"to"in o)&&(a=Ee(ye(o.from),ye(o.to)),o={},o.ms=a.milliseconds,o.M=a.months),i=new we(o),_e(t)&&d(t,"_locale")&&(i._locale=t._locale),i}function Ne(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function He(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 Ee(t,e){var n;return t.isValid()&&e.isValid()?(e=Ce(e,t),t.isBefore(e)?n=He(t,e):(n=He(e,t),n.milliseconds=-n.milliseconds,n.months=-n.months),n):{milliseconds:0,months:0}}function Ue(t){return 0>t?-1*Math.round(-1*t):Math.round(t)}function je(t,e){return function(n,i){var a,o;return null===i||isNaN(+i)||(M(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."),o=n,n=i,i=o),n="string"==typeof n?+n:n,a=ze(n,i),Ge(this,a,t),this}}function Ge(e,n,i,a){var o=n._milliseconds,r=Ue(n._days),s=Ue(n._months);e.isValid()&&(a=null==a?!0:a,o&&e._d.setTime(e._d.valueOf()+o*i),r&&E(e,"Date",H(e,"Date")+r*i),s&&ut(e,H(e,"Month")+s*i),a&&t.updateOffset(e,r||s))}function qe(t,e){var n=t.diff(e,"days",!0);return-6>n?"sameElse":-1>n?"lastWeek":0>n?"lastDay":1>n?"sameDay":2>n?"nextDay":7>n?"nextWeek":"sameElse"}function Ze(e,n){var i=e||ye(),a=Ce(i,this).startOf("day"),o=t.calendarFormat(this,a)||"sameElse",r=n&&(D(n[o])?n[o].call(this,i):n[o]);return this.format(r||this.localeData().calendar(o,this,ye(i)))}function Je(){return new b(this)}function Xe(t,e){var n=y(t)?t:ye(t);return this.isValid()&&n.isValid()?(e=V(p(e)?"millisecond":e),"millisecond"===e?this.valueOf()>n.valueOf():n.valueOf()e-o?(n=t.clone().add(a-1,"months"),i=(e-o)/(o-n)):(n=t.clone().add(a+1,"months"),i=(e-o)/(n-o)),-(a+i)||0}function on(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function rn(){var t=this.clone().utc();return 0o&&(e=o),In.call(this,t,e,n,i,a))}function In(t,e,n,i,a){var o=St(t,e,n,i,a),r=xt(o.year,0,o.dayOfYear);return this.year(r.getUTCFullYear()),this.month(r.getUTCMonth()),this.date(r.getUTCDate()),this}function On(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)}function Rn(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[oa]=k(1e3*("0."+t))}function Ln(){return this._isUTC?"UTC":""}function Vn(){return this._isUTC?"Coordinated Universal Time":""}function Bn(t){return ye(1e3*t)}function Yn(){return ye.apply(null,arguments).parseZone()}function zn(t){return t}function Nn(t,e,n,i){var a=ne(),o=c().set(i,e);return a[n](o,t)}function Hn(t,e,n){if("number"==typeof t&&(e=t,t=void 0),t=t||"",null!=e)return Nn(t,e,n,"month");var i,a=[];for(i=0;12>i;i++)a[i]=Nn(t,i,n,"month");return a}function En(t,e,n,i){"boolean"==typeof t?("number"==typeof e&&(n=e,e=void 0),e=e||""):(e=t,n=e,t=!1,"number"==typeof e&&(n=e,e=void 0),e=e||"");var a=ne(),o=t?a._week.dow:0;if(null!=n)return Nn(e,(n+o)%7,i,"day");var r,s=[];for(r=0;7>r;r++)s[r]=Nn(e,(r+o)%7,i,"day");return s}function Un(t,e){return Hn(t,e,"months")}function jn(t,e){return Hn(t,e,"monthsShort")}function Gn(t,e,n){return En(t,e,n,"weekdays")}function qn(t,e,n){return En(t,e,n,"weekdaysShort")}function Zn(t,e,n){return En(t,e,n,"weekdaysMin")}function Jn(){var t=this._data;return this._milliseconds=Ja(this._milliseconds),this._days=Ja(this._days),this._months=Ja(this._months),t.milliseconds=Ja(t.milliseconds),t.seconds=Ja(t.seconds),t.minutes=Ja(t.minutes),t.hours=Ja(t.hours),t.months=Ja(t.months),t.years=Ja(t.years),this}function Xn(t,e,n,i){var a=ze(e,n);return t._milliseconds+=i*a._milliseconds,t._days+=i*a._days,t._months+=i*a._months,t._bubble()}function Kn(t,e){return Xn(this,t,e,1)}function Qn(t,e){return Xn(this,t,e,-1)}function $n(t){return 0>t?Math.floor(t):Math.ceil(t)}function ti(){var t,e,n,i,a,o=this._milliseconds,r=this._days,s=this._months,l=this._data;return o>=0&&r>=0&&s>=0||0>=o&&0>=r&&0>=s||(o+=864e5*$n(ni(s)+r),r=0,s=0),l.milliseconds=o%1e3,t=x(o/1e3),l.seconds=t%60,e=x(t/60),l.minutes=e%60,n=x(e/60),l.hours=n%24,r+=x(n/24),a=x(ei(r)),s+=a,r-=$n(ni(a)),i=x(s/12),s%=12,l.days=r,l.months=s,l.years=i,this}function ei(t){return 4800*t/146097}function ni(t){return 146097*t/4800}function ii(t){var e,n,i=this._milliseconds;if(t=V(t),"month"===t||"year"===t)return e=this._days+i/864e5,n=this._months+ei(e),"month"===t?n:n/12;switch(e=this._days+Math.round(ni(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 ai(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*k(this._months/12)}function oi(t){return function(){return this.as(t)}}function ri(t){return t=V(t),this[t+"s"]()}function si(t){return function(){return this._data[t]}}function li(){return x(this.days()/7)}function di(t,e,n,i,a){return a.relativeTime(e||1,!!n,t,i)}function ui(t,e,n){var i=ze(t).abs(),a=ho(i.as("s")),o=ho(i.as("m")),r=ho(i.as("h")),s=ho(i.as("d")),l=ho(i.as("M")),d=ho(i.as("y")),u=a=o&&["m"]||o=r&&["h"]||r=s&&["d"]||s=l&&["M"]||l=d&&["y"]||["yy",d];return u[2]=e,u[3]=+t>0,u[4]=n,di.apply(null,u)}function ci(t){return void 0===t?ho:"function"==typeof t?(ho=t,!0):!1}function hi(t,e){return void 0===fo[t]?!1:void 0===e?fo[t]:(fo[t]=e,!0)}function fi(t){var e=this.localeData(),n=ui(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)}function gi(){var t,e,n,i=go(this._milliseconds)/1e3,a=go(this._days),o=go(this._months);t=x(i/60),e=x(t/60),i%=60,t%=60,n=x(o/12),o%=12;var r=n,s=o,l=a,d=e,u=t,c=i,h=this.asSeconds();return h?(0>h?"-":"")+"P"+(r?r+"Y":"")+(s?s+"M":"")+(l?l+"D":"")+(d||u||c?"T":"")+(d?d+"H":"")+(u?u+"M":"")+(c?c+"S":""):"P0D"}var mi,pi;pi=Array.prototype.some?Array.prototype.some:function(t){for(var e=Object(this),n=e.length>>>0,i=0;n>i;i++)if(i in e&&t.call(this,e[i],i,e))return!0;return!1};var vi=t.momentProperties=[],bi=!1,yi={};t.suppressDeprecationWarnings=!1,t.deprecationHandler=null;var xi;xi=Object.keys?Object.keys:function(t){var e,n=[];for(e in t)d(t,e)&&n.push(e);return n};var ki,Si={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},wi={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},_i="Invalid date",Mi="%d",Di=/\d{1,2}/,Ci={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Ti={},Pi={},Ai=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Fi=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Ii={},Oi={},Ri=/\d/,Wi=/\d\d/,Li=/\d{3}/,Vi=/\d{4}/,Bi=/[+-]?\d{6}/,Yi=/\d\d?/,zi=/\d\d\d\d?/,Ni=/\d\d\d\d\d\d?/,Hi=/\d{1,3}/,Ei=/\d{1,4}/,Ui=/[+-]?\d{1,6}/,ji=/\d+/,Gi=/[+-]?\d+/,qi=/Z|[+-]\d\d:?\d\d/gi,Zi=/Z|[+-]\d\d(?::?\d\d)?/gi,Ji=/[+-]?\d+(\.\d{1,3})?/,Xi=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Ki={},Qi={},$i=0,ta=1,ea=2,na=3,ia=4,aa=5,oa=6,ra=7,sa=8;ki=Array.prototype.indexOf?Array.prototype.indexOf:function(t){var e;for(e=0;e=t?""+t:"+"+t}),q(0,["YY",2],0,function(){return this.year()%100}),q(0,["YYYY",4],0,"year"),q(0,["YYYYY",5],0,"year"),q(0,["YYYYYY",6,!0],0,"year"),L("year","y"),Y("year",1),Q("Y",Gi),Q("YY",Yi,Wi),Q("YYYY",Ei,Vi),Q("YYYYY",Ui,Bi),Q("YYYYYY",Ui,Bi),nt(["YYYYY","YYYYYY"],$i),nt("YYYY",function(e,n){n[$i]=2===e.length?t.parseTwoDigitYear(e):k(e)}),nt("YY",function(e,n){n[$i]=t.parseTwoDigitYear(e)}),nt("Y",function(t,e){e[$i]=parseInt(t,10)}),t.parseTwoDigitYear=function(t){return k(t)+(k(t)>68?1900:2e3)};var fa=N("FullYear",!0);q("w",["ww",2],"wo","week"),q("W",["WW",2],"Wo","isoWeek"),L("week","w"),L("isoWeek","W"),Y("week",5),Y("isoWeek",5),Q("w",Yi),Q("ww",Yi,Wi),Q("W",Yi),Q("WW",Yi,Wi),it(["w","ww","W","WW"],function(t,e,n,i){e[i.substr(0,1)]=k(t)});var ga={dow:0,doy:6};q("d",0,"do","day"),q("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),q("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),q("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),q("e",0,0,"weekday"),q("E",0,0,"isoWeekday"),L("day","d"),L("weekday","e"),L("isoWeekday","E"),Y("day",11),Y("weekday",11),Y("isoWeekday",11),Q("d",Yi),Q("e",Yi),Q("E",Yi),Q("dd",function(t,e){return e.weekdaysMinRegex(t)}),Q("ddd",function(t,e){return e.weekdaysShortRegex(t)}),Q("dddd",function(t,e){return e.weekdaysRegex(t)}),it(["dd","ddd","dddd"],function(t,e,n,i){var a=n._locale.weekdaysParse(t,i,n._strict);null!=a?e.d=a:f(n).invalidWeekday=t}),it(["d","e","E"],function(t,e,n,i){e[i]=k(t)});var ma="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),pa="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),va="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),ba=Xi,ya=Xi,xa=Xi;q("H",["HH",2],0,"hour"),q("h",["hh",2],0,Ut),q("k",["kk",2],0,jt),q("hmm",0,0,function(){return""+Ut.apply(this)+G(this.minutes(),2)}),q("hmmss",0,0,function(){return""+Ut.apply(this)+G(this.minutes(),2)+G(this.seconds(),2)}),q("Hmm",0,0,function(){return""+this.hours()+G(this.minutes(),2)}),q("Hmmss",0,0,function(){return""+this.hours()+G(this.minutes(),2)+G(this.seconds(),2)}),Gt("a",!0),Gt("A",!1),L("hour","h"),Y("hour",13),Q("a",qt),Q("A",qt),Q("H",Yi),Q("h",Yi),Q("HH",Yi,Wi),Q("hh",Yi,Wi),Q("hmm",zi),Q("hmmss",Ni),Q("Hmm",zi),Q("Hmmss",Ni),nt(["H","HH"],na),nt(["a","A"],function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t}),nt(["h","hh"],function(t,e,n){e[na]=k(t),f(n).bigHour=!0}),nt("hmm",function(t,e,n){var i=t.length-2;e[na]=k(t.substr(0,i)),e[ia]=k(t.substr(i)),f(n).bigHour=!0}),nt("hmmss",function(t,e,n){var i=t.length-4,a=t.length-2;e[na]=k(t.substr(0,i)),e[ia]=k(t.substr(i,2)),e[aa]=k(t.substr(a)),f(n).bigHour=!0}),nt("Hmm",function(t,e,n){var i=t.length-2;e[na]=k(t.substr(0,i)),e[ia]=k(t.substr(i))}),nt("Hmmss",function(t,e,n){var i=t.length-4,a=t.length-2;e[na]=k(t.substr(0,i)),e[ia]=k(t.substr(i,2)),e[aa]=k(t.substr(a))});var ka,Sa=/[ap]\.?m?\.?/i,wa=N("Hours",!0),_a={calendar:Si,longDateFormat:wi,invalidDate:_i,ordinal:Mi,ordinalParse:Di,relativeTime:Ci,months:da,monthsShort:ua,week:ga,weekdays:ma,weekdaysMin:va,weekdaysShort:pa,meridiemParse:Sa},Ma={},Da=/^\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)?)?/,Ca=/^\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)?)?/,Ta=/Z|[+-]\d\d(?::?\d\d)?/,Pa=[["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}/]],Aa=[["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/]],Fa=/^\/?Date\((\-?\d+)/i; -t.createFromInputFallback=_("moment construction falls back to js Date. This is discouraged and will be removed in 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 Ia=_("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var t=ye.apply(null,arguments);return this.isValid()&&t.isValid()?this>t?this:t:m()}),Oa=_("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var t=ye.apply(null,arguments);return this.isValid()&&t.isValid()?t>this?this:t:m()}),Ra=function(){return Date.now?Date.now():+new Date};Me("Z",":"),Me("ZZ",""),Q("Z",Zi),Q("ZZ",Zi),nt(["Z","ZZ"],function(t,e,n){n._useUTC=!0,n._tzm=De(Zi,t)});var Wa=/([\+\-]|\d\d)/gi;t.updateOffset=function(){};var La=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/,Va=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;ze.fn=we.prototype;var Ba=je(1,"add"),Ya=je(-1,"subtract");t.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",t.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var za=_("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)});q(0,["gg",2],0,function(){return this.weekYear()%100}),q(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Dn("gggg","weekYear"),Dn("ggggg","weekYear"),Dn("GGGG","isoWeekYear"),Dn("GGGGG","isoWeekYear"),L("weekYear","gg"),L("isoWeekYear","GG"),Y("weekYear",1),Y("isoWeekYear",1),Q("G",Gi),Q("g",Gi),Q("GG",Yi,Wi),Q("gg",Yi,Wi),Q("GGGG",Ei,Vi),Q("gggg",Ei,Vi),Q("GGGGG",Ui,Bi),Q("ggggg",Ui,Bi),it(["gggg","ggggg","GGGG","GGGGG"],function(t,e,n,i){e[i.substr(0,2)]=k(t)}),it(["gg","GG"],function(e,n,i,a){n[a]=t.parseTwoDigitYear(e)}),q("Q",0,"Qo","quarter"),L("quarter","Q"),Y("quarter",7),Q("Q",Ri),nt("Q",function(t,e){e[ta]=3*(k(t)-1)}),q("D",["DD",2],"Do","date"),L("date","D"),Y("date",9),Q("D",Yi),Q("DD",Yi,Wi),Q("Do",function(t,e){return t?e._ordinalParse:e._ordinalParseLenient}),nt(["D","DD"],ea),nt("Do",function(t,e){e[ea]=k(t.match(Yi)[0],10)});var Na=N("Date",!0);q("DDD",["DDDD",3],"DDDo","dayOfYear"),L("dayOfYear","DDD"),Y("dayOfYear",4),Q("DDD",Hi),Q("DDDD",Li),nt(["DDD","DDDD"],function(t,e,n){n._dayOfYear=k(t)}),q("m",["mm",2],0,"minute"),L("minute","m"),Y("minute",14),Q("m",Yi),Q("mm",Yi,Wi),nt(["m","mm"],ia);var Ha=N("Minutes",!1);q("s",["ss",2],0,"second"),L("second","s"),Y("second",15),Q("s",Yi),Q("ss",Yi,Wi),nt(["s","ss"],aa);var Ea=N("Seconds",!1);q("S",0,0,function(){return~~(this.millisecond()/100)}),q(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),q(0,["SSS",3],0,"millisecond"),q(0,["SSSS",4],0,function(){return 10*this.millisecond()}),q(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),q(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),q(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),q(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),q(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),L("millisecond","ms"),Y("millisecond",16),Q("S",Hi,Ri),Q("SS",Hi,Wi),Q("SSS",Hi,Li);var Ua;for(Ua="SSSS";Ua.length<=9;Ua+="S")Q(Ua,ji);for(Ua="S";Ua.length<=9;Ua+="S")nt(Ua,Wn);var ja=N("Milliseconds",!1);q("z",0,0,"zoneAbbr"),q("zz",0,0,"zoneName");var Ga=b.prototype;Ga.add=Ba,Ga.calendar=Ze,Ga.clone=Je,Ga.diff=nn,Ga.endOf=mn,Ga.format=sn,Ga.from=ln,Ga.fromNow=dn,Ga.to=un,Ga.toNow=cn,Ga.get=U,Ga.invalidAt=_n,Ga.isAfter=Xe,Ga.isBefore=Ke,Ga.isBetween=Qe,Ga.isSame=$e,Ga.isSameOrAfter=tn,Ga.isSameOrBefore=en,Ga.isValid=Sn,Ga.lang=za,Ga.locale=hn,Ga.localeData=fn,Ga.max=Oa,Ga.min=Ia,Ga.parsingFlags=wn,Ga.set=j,Ga.startOf=gn,Ga.subtract=Ya,Ga.toArray=yn,Ga.toObject=xn,Ga.toDate=bn,Ga.toISOString=rn,Ga.toJSON=kn,Ga.toString=on,Ga.unix=vn,Ga.valueOf=pn,Ga.creationData=Mn,Ga.year=fa,Ga.isLeapYear=bt,Ga.weekYear=Cn,Ga.isoWeekYear=Tn,Ga.quarter=Ga.quarters=On,Ga.month=ct,Ga.daysInMonth=ht,Ga.week=Ga.weeks=Tt,Ga.isoWeek=Ga.isoWeeks=Pt,Ga.weeksInYear=An,Ga.isoWeeksInYear=Pn,Ga.date=Na,Ga.day=Ga.days=Vt,Ga.weekday=Bt,Ga.isoWeekday=Yt,Ga.dayOfYear=Rn,Ga.hour=Ga.hours=wa,Ga.minute=Ga.minutes=Ha,Ga.second=Ga.seconds=Ea,Ga.millisecond=Ga.milliseconds=ja,Ga.utcOffset=Pe,Ga.utc=Fe,Ga.local=Ie,Ga.parseZone=Oe,Ga.hasAlignedHourOffset=Re,Ga.isDST=We,Ga.isLocal=Ve,Ga.isUtcOffset=Be,Ga.isUtc=Ye,Ga.isUTC=Ye,Ga.zoneAbbr=Ln,Ga.zoneName=Vn,Ga.dates=_("dates accessor is deprecated. Use date instead.",Na),Ga.months=_("months accessor is deprecated. Use month instead",ct),Ga.years=_("years accessor is deprecated. Use year instead",fa),Ga.zone=_("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Ae),Ga.isDSTShifted=_("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Le);var qa=Ga,Za=P.prototype;Za.calendar=A,Za.longDateFormat=F,Za.invalidDate=I,Za.ordinal=O,Za.preparse=zn,Za.postformat=zn,Za.relativeTime=R,Za.pastFuture=W,Za.set=C,Za.months=rt,Za.monthsShort=st,Za.monthsParse=dt,Za.monthsRegex=gt,Za.monthsShortRegex=ft,Za.week=Mt,Za.firstDayOfYear=Ct,Za.firstDayOfWeek=Dt,Za.weekdays=It,Za.weekdaysMin=Rt,Za.weekdaysShort=Ot,Za.weekdaysParse=Lt,Za.weekdaysRegex=zt,Za.weekdaysShortRegex=Nt,Za.weekdaysMinRegex=Ht,Za.isPM=Zt,Za.meridiem=Jt,$t("en",{ordinalParse:/\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=_("moment.lang is deprecated. Use moment.locale instead.",$t),t.langData=_("moment.langData is deprecated. Use moment.localeData instead.",ne);var Ja=Math.abs,Xa=oi("ms"),Ka=oi("s"),Qa=oi("m"),$a=oi("h"),to=oi("d"),eo=oi("w"),no=oi("M"),io=oi("y"),ao=si("milliseconds"),oo=si("seconds"),ro=si("minutes"),so=si("hours"),lo=si("days"),uo=si("months"),co=si("years"),ho=Math.round,fo={s:45,m:45,h:22,d:26,M:11},go=Math.abs,mo=we.prototype;mo.abs=Jn,mo.add=Kn,mo.subtract=Qn,mo.as=ii,mo.asMilliseconds=Xa,mo.asSeconds=Ka,mo.asMinutes=Qa,mo.asHours=$a,mo.asDays=to,mo.asWeeks=eo,mo.asMonths=no,mo.asYears=io,mo.valueOf=ai,mo._bubble=ti,mo.get=ri,mo.milliseconds=ao,mo.seconds=oo,mo.minutes=ro,mo.hours=so,mo.days=lo,mo.weeks=li,mo.months=uo,mo.years=co,mo.humanize=fi,mo.toISOString=gi,mo.toString=gi,mo.toJSON=gi,mo.locale=hn,mo.localeData=fn,mo.toIsoString=_("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",gi),mo.lang=za,q("X",0,0,"unix"),q("x",0,0,"valueOf"),Q("x",Gi),Q("X",Ji),nt("X",function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))}),nt("x",function(t,e,n){n._d=new Date(k(t))}),t.version="2.14.1",i(ye),t.fn=qa,t.min=ke,t.max=Se,t.now=Ra,t.utc=c,t.unix=Bn,t.months=Un,t.isDate=s,t.locale=$t,t.invalid=m,t.duration=ze,t.isMoment=y,t.weekdays=Gn,t.parseZone=Yn,t.localeData=ne,t.isDuration=_e,t.monthsShort=jn,t.weekdaysMin=Zn,t.defineLocale=te,t.updateLocale=ee,t.locales=ie,t.weekdaysShort=qn,t.normalizeUnits=V,t.relativeTimeRounding=ci,t.relativeTimeThreshold=hi,t.calendarFormat=qe,t.prototype=qa;var po=t;return po})},{}],7:[function(t,e,n){var i=t(27)();t(26)(i),t(22)(i),t(25)(i),t(21)(i),t(23)(i),t(24)(i),t(28)(i),t(32)(i),t(30)(i),t(31)(i),t(33)(i),t(29)(i),t(34)(i),t(35)(i),t(36)(i),t(37)(i),t(38)(i),t(41)(i),t(39)(i),t(40)(i),t(42)(i),t(43)(i),t(44)(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,41:41,42:42,43:43,44:44,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.options=t.helpers.configMerge({aspectRatio:1},n.options),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),this.getMeta().bar=!0},getBarCount:function(){var t=this,n=0;return e.each(t.chart.data.datasets,function(e,i){var a=t.chart.getDatasetMeta(i);a.bar&&t.chart.isDatasetVisible(i)&&++n},t),n},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,o=a.getMeta(),r=a.getScaleForId(o.xAxisID),s=a.getScaleForId(o.yAxisID),l=s.getBasePixel(),d=a.chart.options.elements.rectangle,u=t.custom||{},c=a.getDataset();e.extend(t,{_xScale:r,_yScale:s,_datasetIndex:a.index,_index:n,_model:{x:a.calculateBarX(n,a.index),y:i?l:a.calculateBarY(n,a.index),label:a.chart.data.labels[n],datasetLabel:c.label,base:i?l:a.calculateBarBase(a.index,n),width:a.calculateBarWidth(n),backgroundColor:u.backgroundColor?u.backgroundColor:e.getValueAtIndexOrDefault(c.backgroundColor,n,d.backgroundColor),borderSkipped:u.borderSkipped?u.borderSkipped:d.borderSkipped,borderColor:u.borderColor?u.borderColor:e.getValueAtIndexOrDefault(c.borderColor,n,d.borderColor),borderWidth:u.borderWidth?u.borderWidth:e.getValueAtIndexOrDefault(c.borderWidth,n,d.borderWidth)}}),t.pivot()},calculateBarBase:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.yAxisID),o=0;if(a.options.stacked){for(var r=n.chart,s=r.data.datasets,l=Number(s[t].data[e]),d=0;t>d;d++){var u=s[d],c=r.getDatasetMeta(d);if(c.bar&&c.yAxisID===a.id&&r.isDatasetVisible(d)){var h=Number(u.data[e]);o+=0>l?Math.min(h,0):Math.max(h,0)}}return a.getPixelForValue(o)}return a.getBasePixel()},getRuler:function(t){var e,n=this,i=n.getMeta(),a=n.getScaleForId(i.xAxisID),o=n.getBarCount();e="category"===a.options.type?a.getPixelForTick(t+1)-a.getPixelForTick(t):a.width/a.ticks.length;var r=e*a.options.categoryPercentage,s=(e-e*a.options.categoryPercentage)/2,l=r/o;if(a.ticks.length!==n.chart.data.labels.length){var d=a.ticks.length/n.chart.data.labels.length;l*=d}var u=l*a.options.barPercentage,c=l-l*a.options.barPercentage;return{datasetCount:o,tickWidth:e,categoryWidth:r,categorySpacing:s,fullBarWidth:l,barWidth:u,barSpacing:c}},calculateBarWidth:function(t){var e=this.getScaleForId(this.getMeta().xAxisID);if(e.options.barThickness)return e.options.barThickness;var n=this.getRuler(t);return e.options.stacked?n.categoryWidth:n.barWidth},getBarIndex:function(t){var e,n,i=0;for(n=0;t>n;++n)e=this.chart.getDatasetMeta(n),e.bar&&this.chart.isDatasetVisible(n)&&++i;return i},calculateBarX:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.xAxisID),o=n.getBarIndex(e),r=n.getRuler(t),s=a.getPixelForValue(null,t,e,n.chart.isCombo);return s-=n.chart.isCombo?r.tickWidth/2:0,a.options.stacked?s+r.categoryWidth/2+r.categorySpacing:s+r.barWidth/2+r.categorySpacing+r.barWidth*o+r.barSpacing/2+r.barSpacing*o},calculateBarY:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.yAxisID),o=Number(n.getDataset().data[t]);if(a.options.stacked){for(var r=0,s=0,l=0;e>l;l++){var d=n.chart.data.datasets[l],u=n.chart.getDatasetMeta(l);if(u.bar&&u.yAxisID===a.id&&n.chart.isDatasetVisible(l)){var c=Number(d.data[t]);0>c?s+=c||0:r+=c||0}}return 0>o?a.getPixelForValue(s+o):a.getPixelForValue(r+o)}return a.getPixelForValue(o)},draw:function(t){var n=this,i=t||1;e.each(n.getMeta().data,function(t,e){var a=n.getDataset().data[e];null===a||void 0===a||isNaN(a)||t.transition(i).draw()},n)},setHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t._index,a=t.custom||{},o=t._model;o.backgroundColor=a.hoverBackgroundColor?a.hoverBackgroundColor:e.getValueAtIndexOrDefault(n.hoverBackgroundColor,i,e.getHoverColor(o.backgroundColor)),o.borderColor=a.hoverBorderColor?a.hoverBorderColor:e.getValueAtIndexOrDefault(n.hoverBorderColor,i,e.getHoverColor(o.borderColor)),o.borderWidth=a.hoverBorderWidth?a.hoverBorderWidth:e.getValueAtIndexOrDefault(n.hoverBorderWidth,i,o.borderWidth)},removeHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t._index,a=t.custom||{},o=t._model,r=this.chart.options.elements.rectangle;o.backgroundColor=a.backgroundColor?a.backgroundColor:e.getValueAtIndexOrDefault(n.backgroundColor,i,r.backgroundColor),o.borderColor=a.borderColor?a.borderColor:e.getValueAtIndexOrDefault(n.borderColor,i,r.borderColor),o.borderWidth=a.borderWidth?a.borderWidth:e.getValueAtIndexOrDefault(n.borderWidth,i,r.borderWidth)}}),t.defaults.horizontalBar={hover:{mode:"label"},scales:{xAxes:[{type:"linear",position:"bottom"}],yAxes:[{position:"left",type:"category",categoryPercentage:.8,barPercentage:.9,gridLines:{offsetGridLines:!0}}]},elements:{rectangle:{borderSkipped:"left"}},tooltips:{callbacks:{title:function(t,e){var n="";return t.length>0&&(t[0].yLabel?n=t[0].yLabel:e.labels.length>0&&t[0].indexc;c++)e.lineTo.apply(e,t(c));e.fill(),n.borderWidth&&e.stroke()},inRange:function(t,e){var n=this._view,i=!1;return n&&(i=n.x=n.y-n.height/2&&e<=n.y+n.height/2&&t>=n.x&&t<=n.base:e>=n.y-n.height/2&&e<=n.y+n.height/2&&t>=n.base&&t<=n.x),i}}),t.pivot()},calculateBarBase:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.xAxisID),o=0;if(a.options.stacked){for(var r=n.chart,s=r.data.datasets,l=Number(s[t].data[e]),d=0;t>d;d++){var u=s[d],c=r.getDatasetMeta(d);if(c.bar&&c.xAxisID===a.id&&r.isDatasetVisible(d)){var h=Number(u.data[e]);o+=0>l?Math.min(h,0):Math.max(h,0)}}return a.getPixelForValue(o)}return a.getBasePixel()},getRuler:function(t){var e,n=this,i=n.getMeta(),a=n.getScaleForId(i.yAxisID),o=n.getBarCount();e="category"===a.options.type?a.getPixelForTick(t+1)-a.getPixelForTick(t):a.width/a.ticks.length;var r=e*a.options.categoryPercentage,s=(e-e*a.options.categoryPercentage)/2,l=r/o;if(a.ticks.length!==n.chart.data.labels.length){var d=a.ticks.length/n.chart.data.labels.length;l*=d}var u=l*a.options.barPercentage,c=l-l*a.options.barPercentage;return{datasetCount:o,tickHeight:e,categoryHeight:r,categorySpacing:s,fullBarHeight:l,barHeight:u,barSpacing:c}},calculateBarHeight:function(t){var e=this,n=e.getScaleForId(e.getMeta().yAxisID);if(n.options.barThickness)return n.options.barThickness;var i=e.getRuler(t);return n.options.stacked?i.categoryHeight:i.barHeight},calculateBarX:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.xAxisID),o=Number(n.getDataset().data[t]);if(a.options.stacked){for(var r=0,s=0,l=0;e>l;l++){var d=n.chart.data.datasets[l],u=n.chart.getDatasetMeta(l);if(u.bar&&u.xAxisID===a.id&&n.chart.isDatasetVisible(l)){var c=Number(d.data[t]);0>c?s+=c||0:r+=c||0}}return 0>o?a.getPixelForValue(s+o):a.getPixelForValue(r+o)}return a.getPixelForValue(o)},calculateBarY:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.yAxisID),o=n.getBarIndex(e),r=n.getRuler(t),s=a.getPixelForValue(null,t,e,n.chart.isCombo);return s-=n.chart.isCombo?r.tickHeight/2:0,a.options.stacked?s+r.categoryHeight/2+r.categorySpacing:s+r.barHeight/2+r.categorySpacing+r.barHeight*o+r.barSpacing/2+r.barSpacing*o}})}},{}],16:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.bubble={hover:{mode:"single"},scales:{xAxes:[{type:"linear",position:"bottom",id:"x-axis-0"}],yAxes:[{type:"linear",position:"left",id:"y-axis-0"}]},tooltips:{callbacks:{title:function(){return""},label:function(t,e){var n=e.datasets[t.datasetIndex].label||"",i=e.datasets[t.datasetIndex].data[t.index];return n+": ("+i.x+", "+i.y+", "+i.r+")"}}}},t.controllers.bubble=t.DatasetController.extend({dataElementType:t.elements.Point,update:function(t){var n=this,i=n.getMeta(),a=i.data;e.each(a,function(e,i){n.updateElement(e,i,t)})},updateElement:function(n,i,a){var o=this,r=o.getMeta(),s=o.getScaleForId(r.xAxisID),l=o.getScaleForId(r.yAxisID),d=n.custom||{},u=o.getDataset(),c=u.data[i],h=o.chart.options.elements.point,f=o.index;e.extend(n,{_xScale:s,_yScale:l,_datasetIndex:f,_index:i,_model:{x:a?s.getPixelForDecimal(.5):s.getPixelForValue("object"==typeof c?c:NaN,i,f,o.chart.isCombo),y:a?l.getBasePixel():l.getPixelForValue(c,i,f),radius:a?0:d.radius?d.radius:o.getRadius(c),hitRadius:d.hitRadius?d.hitRadius:e.getValueAtIndexOrDefault(u.hitRadius,i,h.hitRadius)}}),t.DatasetController.prototype.removeHoverStyle.call(o,n,h);var g=n._model;g.skip=d.skip?d.skip:isNaN(g.x)||isNaN(g.y),n.pivot()},getRadius:function(t){return t.r||this.chart.options.elements.point.radius},setHoverStyle:function(n){var i=this;t.DatasetController.prototype.setHoverStyle.call(i,n);var a=i.chart.data.datasets[n._datasetIndex],o=n._index,r=n.custom||{},s=n._model;s.radius=r.hoverRadius?r.hoverRadius:e.getValueAtIndexOrDefault(a.hoverRadius,o,i.chart.options.elements.point.hoverRadius)+i.getRadius(a.data[o])},removeHoverStyle:function(e){var n=this;t.DatasetController.prototype.removeHoverStyle.call(n,e,n.chart.options.elements.point);var i=n.chart.data.datasets[e._datasetIndex].data[e._index],a=e.custom||{},o=e._model;o.radius=a.radius?a.radius:n.getRadius(i)}})}},{}],17:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=t.defaults;n.doughnut={animation:{animateRotate:!0,animateScale:!1},aspectRatio:1,hover:{mode:"single"},legendCallback:function(t){var e=[];e.push('

    ');var n=t.data,i=n.datasets,a=n.labels;if(i.length)for(var o=0;o'),a[o]&&e.push(a[o]),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 o=t.getDatasetMeta(0),r=n.datasets[0],s=o.data[a],l=s&&s.custom||{},d=e.getValueAtIndexOrDefault,u=t.options.elements.arc,c=l.backgroundColor?l.backgroundColor:d(r.backgroundColor,a,u.backgroundColor),h=l.borderColor?l.borderColor:d(r.borderColor,a,u.borderColor),f=l.borderWidth?l.borderWidth:d(r.borderWidth,a,u.borderWidth);return{text:i,fillStyle:c,strokeStyle:h,lineWidth:f,hidden:isNaN(r.data[a])||o.data[a].hidden,index:a}}):[]}},onClick:function(t,e){var n,i,a,o=e.index,r=this.chart;for(n=0,i=(r.data.datasets||[]).length;i>n;++n)a=r.getDatasetMeta(n),a.data[o].hidden=!a.data[o].hidden;r.update()}},cutoutPercentage:50,rotation:Math.PI*-.5,circumference:2*Math.PI,tooltips:{callbacks:{title:function(){return""},label:function(t,e){return e.labels[t.index]+": "+e.datasets[t.datasetIndex].data[t.index]}}}},n.pie=e.clone(n.doughnut),e.extend(n.pie,{cutoutPercentage:0}),t.controllers.doughnut=t.controllers.pie=t.DatasetController.extend({dataElementType:t.elements.Arc,linkScales:e.noop,getRingIndex:function(t){for(var e=0,n=0;t>n;++n)this.chart.isDatasetVisible(n)&&++e;return e},update:function(t){var n=this,i=n.chart,a=i.chartArea,o=i.options,r=o.elements.arc,s=a.right-a.left-r.borderWidth,l=a.bottom-a.top-r.borderWidth,d=Math.min(s,l),u={x:0,y:0},c=n.getMeta(),h=o.cutoutPercentage,f=o.circumference;if(f<2*Math.PI){var g=o.rotation%(2*Math.PI);g+=2*Math.PI*(g>=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=0>=g&&m>=0||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,S=h/100,w={x:x?-1:Math.min(p.x*(p.x<0?1:S),v.x*(v.x<0?1:S)),y:k?-1:Math.min(p.y*(p.y<0?1:S),v.y*(v.y<0?1:S))},_={x:b?1:Math.max(p.x*(p.x>0?1:S),v.x*(v.x>0?1:S)),y:y?1:Math.max(p.y*(p.y>0?1:S),v.y*(v.y>0?1:S))},M={width:.5*(_.x-w.x),height:.5*(_.y-w.y)};d=Math.min(s/M.width,l/M.height),u={x:(_.x+w.x)*-.5,y:(_.y+w.y)*-.5}}i.borderWidth=n.getMaxBorderWidth(c.data),i.outerRadius=Math.max((d-i.borderWidth)/2,0),i.innerRadius=Math.max(h?i.outerRadius/100*h:1,0),i.radiusLength=(i.outerRadius-i.innerRadius)/i.getVisibleDatasetCount(),i.offsetX=u.x*i.outerRadius,i.offsetY=u.y*i.outerRadius,c.total=n.calculateTotal(),n.outerRadius=i.outerRadius-i.radiusLength*n.getRingIndex(n.index),n.innerRadius=n.outerRadius-i.radiusLength,e.each(c.data,function(e,i){n.updateElement(e,i,t)})},updateElement:function(t,n,i){var a=this,o=a.chart,r=o.chartArea,s=o.options,l=s.animation,d=(r.left+r.right)/2,u=(r.top+r.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:d+o.offsetX,y:u+o.offsetY,startAngle:c,endAngle:h,circumference:g,outerRadius:p,innerRadius:m,label:v(f.label,n,o.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,o=t.length,r=0;o>r;r++)e=t[r]._model?t[r]._model.borderWidth:0,n=t[r]._chart?t[r]._chart.config.data.datasets[a].hoverBorderWidth:0,i=e>i?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,addElementAndReset:function(n){var i=this,a=i.chart.options,o=i.getMeta();t.DatasetController.prototype.addElementAndReset.call(i,n),e(i.getDataset(),a)&&0!==o.dataset._model.tension&&i.updateBezierControlPoints()},update:function(t){var i,a,o,r=this,s=r.getMeta(),l=s.dataset,d=s.data||[],u=r.chart.options,c=u.elements.line,h=r.getScaleForId(s.yAxisID),f=r.getDataset(),g=e(f,u);for(g&&(o=l.custom||{},void 0!==f.tension&&void 0===f.lineTension&&(f.lineTension=f.tension),l._scale=h,l._datasetIndex=r.index,l._children=d,l._model={spanGaps:f.spanGaps?f.spanGaps:u.spanGaps,tension:o.tension?o.tension:n.getValueOrDefault(f.lineTension,c.tension),backgroundColor:o.backgroundColor?o.backgroundColor:f.backgroundColor||c.backgroundColor,borderWidth:o.borderWidth?o.borderWidth:f.borderWidth||c.borderWidth,borderColor:o.borderColor?o.borderColor:f.borderColor||c.borderColor,borderCapStyle:o.borderCapStyle?o.borderCapStyle:f.borderCapStyle||c.borderCapStyle,borderDash:o.borderDash?o.borderDash:f.borderDash||c.borderDash,borderDashOffset:o.borderDashOffset?o.borderDashOffset:f.borderDashOffset||c.borderDashOffset,borderJoinStyle:o.borderJoinStyle?o.borderJoinStyle:f.borderJoinStyle||c.borderJoinStyle,fill:o.fill?o.fill:void 0!==f.fill?f.fill:c.fill,steppedLine:o.steppedLine?o.steppedLine:n.getValueOrDefault(f.steppedLine,c.stepped),cubicInterpolationMode:o.cubicInterpolationMode?o.cubicInterpolationMode:n.getValueOrDefault(f.cubicInterpolationMode,c.cubicInterpolationMode),scaleTop:h.top,scaleBottom:h.bottom,scaleZero:h.getBasePixel()},l.pivot()),i=0,a=d.length;a>i;++i)r.updateElement(d[i],i,t);for(g&&0!==l._model.tension&&r.updateBezierControlPoints(),i=0,a=d.length;a>i;++i)d[i].pivot()},getPointBackgroundColor:function(t,e){var i=this.chart.options.elements.point.backgroundColor,a=this.getDataset(),o=t.custom||{};return o.backgroundColor?i=o.backgroundColor:a.pointBackgroundColor?i=n.getValueAtIndexOrDefault(a.pointBackgroundColor,e,i):a.backgroundColor&&(i=a.backgroundColor),i},getPointBorderColor:function(t,e){var i=this.chart.options.elements.point.borderColor,a=this.getDataset(),o=t.custom||{};return o.borderColor?i=o.borderColor:a.pointBorderColor?i=n.getValueAtIndexOrDefault(a.pointBorderColor,e,i):a.borderColor&&(i=a.borderColor),i},getPointBorderWidth:function(t,e){var i=this.chart.options.elements.point.borderWidth,a=this.getDataset(),o=t.custom||{};return o.borderWidth?i=o.borderWidth:a.pointBorderWidth?i=n.getValueAtIndexOrDefault(a.pointBorderWidth,e,i):a.borderWidth&&(i=a.borderWidth),i},updateElement:function(t,e,i){var a,o,r=this,s=r.getMeta(),l=t.custom||{},d=r.getDataset(),u=r.index,c=d.data[e],h=r.getScaleForId(s.yAxisID),f=r.getScaleForId(s.xAxisID),g=r.chart.options.elements.point,m=r.chart.data.labels||[],p=1===m.length||1===d.data.length||r.chart.isCombo;void 0!==d.radius&&void 0===d.pointRadius&&(d.pointRadius=d.radius),void 0!==d.hitRadius&&void 0===d.pointHitRadius&&(d.pointHitRadius=d.hitRadius),a=f.getPixelForValue("object"==typeof c?c:NaN,e,u,p),o=i?h.getBasePixel():r.calculatePointY(c,e,u),t._xScale=f,t._yScale=h,t._datasetIndex=u,t._index=e,t._model={x:a,y:o,skip:l.skip||isNaN(a)||isNaN(o),radius:l.radius||n.getValueAtIndexOrDefault(d.pointRadius,e,g.radius),pointStyle:l.pointStyle||n.getValueAtIndexOrDefault(d.pointStyle,e,g.pointStyle),backgroundColor:r.getPointBackgroundColor(t,e),borderColor:r.getPointBorderColor(t,e),borderWidth:r.getPointBorderWidth(t,e),tension:s.dataset._model?s.dataset._model.tension:0,steppedLine:s.dataset._model?s.dataset._model.steppedLine:!1,hitRadius:l.hitRadius||n.getValueAtIndexOrDefault(d.pointHitRadius,e,g.hitRadius)}},calculatePointY:function(t,e,n){var i,a,o,r=this,s=r.chart,l=r.getMeta(),d=r.getScaleForId(l.yAxisID),u=0,c=0;if(d.options.stacked){for(i=0;n>i;i++)if(a=s.data.datasets[i],o=s.getDatasetMeta(i),"line"===o.type&&o.yAxisID===d.id&&s.isDatasetVisible(i)){var h=Number(d.getRightValue(a.data[e]));0>h?c+=h||0:u+=h||0}var f=Number(d.getRightValue(t));return 0>f?d.getPixelForValue(c+f):d.getPixelForValue(u+f)}return d.getPixelForValue(t)},updateBezierControlPoints:function(){function t(t,e,n){return Math.max(Math.min(t,n),e)}var e=this,i=e.getMeta(),a=e.chart.chartArea,o=i.data||[];i.dataset._model.spanGaps&&(o=o.filter(function(t){return!t._model.skip}));var r,s,l,d,u;if("monotone"==i.dataset._model.cubicInterpolationMode)n.splineCurveMonotone(o);else for(r=0,s=o.length;s>r;++r)l=o[r],d=l._model,u=n.splineCurve(n.previousItem(o,r)._model,d,n.nextItem(o,r)._model,i.dataset._model.tension),d.controlPointPreviousX=u.previous.x,d.controlPointPreviousY=u.previous.y,d.controlPointNextX=u.next.x,d.controlPointNextY=u.next.y;if(e.chart.options.elements.line.capBezierPoints)for(r=0,s=o.length;s>r;++r)d=o[r]._model,d.controlPointPreviousX=t(d.controlPointPreviousX,a.left,a.right),d.controlPointPreviousY=t(d.controlPointPreviousY,a.top,a.bottom),d.controlPointNextX=t(d.controlPointNextX,a.left,a.right),d.controlPointNextY=t(d.controlPointNextY,a.top,a.bottom)},draw:function(t){var n,i,a=this,o=a.getMeta(),r=o.data||[],s=t||1;for(n=0,i=r.length;i>n;++n)r[n].transition(s);for(e(a.getDataset(),a.chart.options)&&o.dataset.transition(s).draw(),n=0,i=r.length;i>n;++n)r[n].draw()},setHoverStyle:function(t){var e=this.chart.data.datasets[t._datasetIndex],i=t._index,a=t.custom||{},o=t._model;o.radius=a.hoverRadius||n.getValueAtIndexOrDefault(e.pointHoverRadius,i,this.chart.options.elements.point.hoverRadius),o.backgroundColor=a.hoverBackgroundColor||n.getValueAtIndexOrDefault(e.pointHoverBackgroundColor,i,n.getHoverColor(o.backgroundColor)),o.borderColor=a.hoverBorderColor||n.getValueAtIndexOrDefault(e.pointHoverBorderColor,i,n.getHoverColor(o.borderColor)),o.borderWidth=a.hoverBorderWidth||n.getValueAtIndexOrDefault(e.pointHoverBorderWidth,i,o.borderWidth)},removeHoverStyle:function(t){var e=this,i=e.chart.data.datasets[t._datasetIndex],a=t._index,o=t.custom||{},r=t._model;void 0!==i.radius&&void 0===i.pointRadius&&(i.pointRadius=i.radius),r.radius=o.radius||n.getValueAtIndexOrDefault(i.pointRadius,a,e.chart.options.elements.point.radius),r.backgroundColor=e.getPointBackgroundColor(t,a),r.borderColor=e.getPointBorderColor(t,a),r.borderWidth=e.getPointBorderWidth(t,a)}})}},{}],19:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.polarArea={scale:{type:"radialLinear",lineArc:!0,ticks:{beginAtZero:!0}},animation:{animateRotate:!0,animateScale:!0},startAngle:-.5*Math.PI,aspectRatio:1,legendCallback:function(t){var e=[];e.push('
    ');var n=t.data,i=n.datasets,a=n.labels;if(i.length)for(var o=0;o'),a[o]&&e.push(a[o]),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 o=t.getDatasetMeta(0),r=n.datasets[0],s=o.data[a],l=s.custom||{},d=e.getValueAtIndexOrDefault,u=t.options.elements.arc,c=l.backgroundColor?l.backgroundColor:d(r.backgroundColor,a,u.backgroundColor),h=l.borderColor?l.borderColor:d(r.borderColor,a,u.borderColor),f=l.borderWidth?l.borderWidth:d(r.borderWidth,a,u.borderWidth);return{text:i,fillStyle:c,strokeStyle:h,lineWidth:f,hidden:isNaN(r.data[a])||o.data[a].hidden,index:a}}):[]}},onClick:function(t,e){ -var n,i,a,o=e.index,r=this.chart;for(n=0,i=(r.data.datasets||[]).length;i>n;++n)a=r.getDatasetMeta(n),a.data[o].hidden=!a.data[o].hidden;r.update()}},tooltips:{callbacks:{title:function(){return""},label:function(t,e){return e.labels[t.index]+": "+t.yLabel}}}},t.controllers.polarArea=t.DatasetController.extend({dataElementType:t.elements.Arc,linkScales:e.noop,update:function(t){var n=this,i=n.chart,a=i.chartArea,o=n.getMeta(),r=i.options,s=r.elements.arc,l=Math.min(a.right-a.left,a.bottom-a.top);i.outerRadius=Math.max((l-s.borderWidth/2)/2,0),i.innerRadius=Math.max(r.cutoutPercentage?i.outerRadius/100*r.cutoutPercentage:1,0),i.radiusLength=(i.outerRadius-i.innerRadius)/i.getVisibleDatasetCount(),n.outerRadius=i.outerRadius-i.radiusLength*n.index,n.innerRadius=n.outerRadius-i.radiusLength,o.count=n.countVisibleElements(),e.each(o.data,function(e,i){n.updateElement(e,i,t)})},updateElement:function(t,n,i){for(var a=this,o=a.chart,r=a.getDataset(),s=o.options,l=s.animation,d=o.scale,u=e.getValueAtIndexOrDefault,c=o.data.labels,h=a.calculateCircumference(r.data[n]),f=d.xCenter,g=d.yCenter,m=0,p=a.getMeta(),v=0;n>v;++v)isNaN(r.data[v])||p.data[v].hidden||++m;var b=s.startAngle,y=t.hidden?0:d.getDistanceFromCenterForValue(r.data[n]),x=b+h*m,k=x+(t.hidden?0:h),S=l.animateScale?0:d.getDistanceFromCenterForValue(r.data[n]);e.extend(t,{_datasetIndex:a.index,_index:n,_scale:d,_model:{x:f,y:g,innerRadius:0,outerRadius:i?S:y,startAngle:i&&l.animateRotate?b:x,endAngle:i&&l.animateRotate?b:k,label:u(c,n,c[n])}}),a.removeHoverStyle(t),t.pivot()},removeHoverStyle:function(e){t.DatasetController.prototype.removeHoverStyle.call(this,e,this.chart.options.elements.arc)},countVisibleElements:function(){var t=this.getDataset(),n=this.getMeta(),i=0;return e.each(n.data,function(e,n){isNaN(t.data[n])||e.hidden||i++}),i},calculateCircumference:function(t){var e=this.getMeta().count;return e>0&&!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={scale:{type:"radialLinear"},elements:{line:{tension:0}}},t.controllers.radar=t.DatasetController.extend({datasetElementType:t.elements.Line,dataElementType:t.elements.Point,linkScales:e.noop,addElementAndReset:function(e){t.DatasetController.prototype.addElementAndReset.call(this,e),this.updateBezierControlPoints()},update:function(t){var n=this,i=n.getMeta(),a=i.dataset,o=i.data,r=a.custom||{},s=n.getDataset(),l=n.chart.options.elements.line,d=n.chart.scale;void 0!==s.tension&&void 0===s.lineTension&&(s.lineTension=s.tension),e.extend(i.dataset,{_datasetIndex:n.index,_children:o,_loop:!0,_model:{tension:r.tension?r.tension:e.getValueOrDefault(s.lineTension,l.tension),backgroundColor:r.backgroundColor?r.backgroundColor:s.backgroundColor||l.backgroundColor,borderWidth:r.borderWidth?r.borderWidth:s.borderWidth||l.borderWidth,borderColor:r.borderColor?r.borderColor:s.borderColor||l.borderColor,fill:r.fill?r.fill:void 0!==s.fill?s.fill:l.fill,borderCapStyle:r.borderCapStyle?r.borderCapStyle:s.borderCapStyle||l.borderCapStyle,borderDash:r.borderDash?r.borderDash:s.borderDash||l.borderDash,borderDashOffset:r.borderDashOffset?r.borderDashOffset:s.borderDashOffset||l.borderDashOffset,borderJoinStyle:r.borderJoinStyle?r.borderJoinStyle:s.borderJoinStyle||l.borderJoinStyle,scaleTop:d.top,scaleBottom:d.bottom,scaleZero:d.getBasePosition()}}),i.dataset.pivot(),e.each(o,function(e,i){n.updateElement(e,i,t)},n),n.updateBezierControlPoints()},updateElement:function(t,n,i){var a=this,o=t.custom||{},r=a.getDataset(),s=a.chart.scale,l=a.chart.options.elements.point,d=s.getPointPositionForValue(n,r.data[n]);e.extend(t,{_datasetIndex:a.index,_index:n,_scale:s,_model:{x:i?s.xCenter:d.x,y:i?s.yCenter:d.y,tension:o.tension?o.tension:e.getValueOrDefault(r.tension,a.chart.options.elements.line.tension),radius:o.radius?o.radius:e.getValueAtIndexOrDefault(r.pointRadius,n,l.radius),backgroundColor:o.backgroundColor?o.backgroundColor:e.getValueAtIndexOrDefault(r.pointBackgroundColor,n,l.backgroundColor),borderColor:o.borderColor?o.borderColor:e.getValueAtIndexOrDefault(r.pointBorderColor,n,l.borderColor),borderWidth:o.borderWidth?o.borderWidth:e.getValueAtIndexOrDefault(r.pointBorderWidth,n,l.borderWidth),pointStyle:o.pointStyle?o.pointStyle:e.getValueAtIndexOrDefault(r.pointStyle,n,l.pointStyle),hitRadius:o.hitRadius?o.hitRadius:e.getValueAtIndexOrDefault(r.hitRadius,n,l.hitRadius)}}),t._model.skip=o.skip?o.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 o=i._model,r=e.splineCurve(e.previousItem(n.data,a,!0)._model,o,e.nextItem(n.data,a,!0)._model,o.tension);o.controlPointPreviousX=Math.max(Math.min(r.previous.x,t.right),t.left),o.controlPointPreviousY=Math.max(Math.min(r.previous.y,t.bottom),t.top),o.controlPointNextX=Math.max(Math.min(r.next.x,t.right),t.left),o.controlPointNextY=Math.max(Math.min(r.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,o=t._model;o.radius=i.hoverRadius?i.hoverRadius:e.getValueAtIndexOrDefault(n.pointHoverRadius,a,this.chart.options.elements.point.hoverRadius),o.backgroundColor=i.hoverBackgroundColor?i.hoverBackgroundColor:e.getValueAtIndexOrDefault(n.pointHoverBackgroundColor,a,e.getHoverColor(o.backgroundColor)),o.borderColor=i.hoverBorderColor?i.hoverBorderColor:e.getValueAtIndexOrDefault(n.pointHoverBorderColor,a,e.getHoverColor(o.borderColor)),o.borderWidth=i.hoverBorderWidth?i.hoverBorderWidth:e.getValueAtIndexOrDefault(n.pointHoverBorderWidth,a,o.borderWidth)},removeHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t.custom||{},a=t._index,o=t._model,r=this.chart.options.elements.point;o.radius=i.radius?i.radius:e.getValueAtIndexOrDefault(n.radius,a,r.radius),o.backgroundColor=i.backgroundColor?i.backgroundColor:e.getValueAtIndexOrDefault(n.pointBackgroundColor,a,r.backgroundColor),o.borderColor=i.borderColor?i.borderColor:e.getValueAtIndexOrDefault(n.pointBorderColor,a,r.borderColor),o.borderWidth=i.borderWidth?i.borderWidth:e.getValueAtIndexOrDefault(n.pointBorderWidth,a,r.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 o=0;o1&&(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(),o=(a-e)/t.frameDuration;t.dropFrames+=o,t.animations.length>0&&t.requestAnimationFrame()}}}},{}],22:[function(t,e,n){"use strict";e.exports=function(t){var e=t.canvasHelpers={};e.drawPoint=function(t,e,n,i,a){var o,r,s,l,d,u;if("object"==typeof e&&(o=e.toString(),"[object HTMLImageElement]"===o||"[object HTMLCanvasElement]"===o))return void t.drawImage(e,i-e.width/2,a-e.height/2);if(!(isNaN(n)||0>=n)){switch(e){default:t.beginPath(),t.arc(i,a,n,0,2*Math.PI),t.closePath(),t.fill();break;case"triangle":t.beginPath(),r=3*n/Math.sqrt(3),d=r*Math.sqrt(3)/2,t.moveTo(i-r/2,a+d/3),t.lineTo(i+r/2,a+d/3),t.lineTo(i,a-2*d/3),t.closePath(),t.fill();break;case"rect":u=1/Math.SQRT2*n,t.beginPath(),t.fillRect(i-u,a-u,2*u,2*u),t.strokeRect(i-u,a-u,2*u,2*u);break;case"rectRot":u=1/Math.SQRT2*n,t.beginPath(),t.moveTo(i-u,a),t.lineTo(i,a+u),t.lineTo(i+u,a),t.lineTo(i,a-u),t.closePath(),t.fill();break;case"cross":t.beginPath(),t.moveTo(i,a+n),t.lineTo(i,a-n),t.moveTo(i-n,a),t.lineTo(i+n,a),t.closePath();break;case"crossRot":t.beginPath(),s=Math.cos(Math.PI/4)*n,l=Math.sin(Math.PI/4)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l),t.moveTo(i-s,a+l),t.lineTo(i+s,a-l),t.closePath();break;case"star":t.beginPath(),t.moveTo(i,a+n),t.lineTo(i,a-n),t.moveTo(i-n,a),t.lineTo(i+n,a),s=Math.cos(Math.PI/4)*n,l=Math.sin(Math.PI/4)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l),t.moveTo(i-s,a+l),t.lineTo(i+s,a-l),t.closePath();break;case"line":t.beginPath(),t.moveTo(i-n,a),t.lineTo(i+n,a),t.closePath();break;case"dash":t.beginPath(),t.moveTo(i,a),t.lineTo(i+n,a),t.closePath()}t.stroke()}}}},{}],23:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.types={},t.instances={},t.controllers={},t.Controller=function(n){return this.chart=n,this.config=n.config,this.options=this.config.options=e.configMerge(t.defaults.global,t.defaults[this.config.type],this.config.options||{}),this.id=e.uid(),Object.defineProperty(this,"data",{get:function(){return this.config.data}}),t.instances[this.id]=this,this.options.responsive&&this.resize(!0),this.initialize(),this},e.extend(t.Controller.prototype,{initialize:function(){var e=this;return t.plugins.notify("beforeInit",[e]),e.bindEvents(),e.ensureScalesHaveIDs(),e.buildOrUpdateControllers(),e.buildScales(),e.updateLayout(),e.resetElements(),e.initToolTip(),e.update(),t.plugins.notify("afterInit",[e]),e},clear:function(){return e.clear(this.chart),this},stop:function(){return t.animationService.cancelAnimation(this),this},resize:function(n){var i=this,a=i.chart,o=a.canvas,r=e.getMaximumWidth(o),s=a.aspectRatio,l=i.options.maintainAspectRatio&&isNaN(s)===!1&&isFinite(s)&&0!==s?r/s:e.getMaximumHeight(o),d=a.width!==r||a.height!==l;if(!d)return i;o.width=a.width=r,o.height=a.height=l,e.retinaScale(a);var u={width:r,height:l};return t.plugins.notify("resize",[i,u]),i.options.onResize&&i.options.onResize(i,u),n||(i.stop(),i.update(i.options.responsiveAnimationDuration)),i},ensureScalesHaveIDs:function(){var t=this.options,n=t.scales||{},i=t.scale;e.each(n.xAxes,function(t,e){t.id=t.id||"x-axis-"+e}),e.each(n.yAxes,function(t,e){t.id=t.id||"y-axis-"+e}),i&&(i.id=i.id||"scale")},buildScales:function(){var n=this,i=n.options,a=n.scales={},o=[];i.scales&&(o=o.concat((i.scales.xAxes||[]).map(function(t){return{options:t,dtype:"category"}}),(i.scales.yAxes||[]).map(function(t){return{options:t,dtype:"linear"}}))),i.scale&&o.push({options:i.scale,dtype:"radialLinear",isDefault:!0}),e.each(o,function(i){var o=i.options,r=e.getValueOrDefault(o.type,i.dtype),s=t.scaleService.getScaleConstructor(r);if(s){var l=new s({id:o.id,options:o,ctx:n.chart.ctx,chart:n});a[l.id]=l,i.isDefault&&(n.scale=l)}}),t.scaleService.addScalesToLayout(this)},updateLayout:function(){t.layoutService.update(this,this.chart.width,this.chart.height)},buildOrUpdateControllers:function(){var n=this,i=[],a=[];if(e.each(n.data.datasets,function(e,o){var r=n.getDatasetMeta(o);r.type||(r.type=e.type||n.config.type),i.push(r.type),r.controller?r.controller.updateIndex(o):(r.controller=new t.controllers[r.type](n,o),a.push(r.controller))},n),i.length>1)for(var o=1;oe;++e)i.getDatasetMeta(e).controller.update();t.plugins.notify("afterDatasetsUpdate",[i])}},render:function(n,i){var a=this;t.plugins.notify("beforeRender",[a]);var o=a.options.animation;if(o&&("undefined"!=typeof n&&0!==n||"undefined"==typeof n&&0!==o.duration)){var r=new t.Animation;r.numSteps=(n||o.duration)/16.66,r.easing=o.easing,r.render=function(t,n){var i=e.easingEffects[n.easing],a=n.currentStep/n.numSteps,o=i(a);t.draw(o,a,n.currentStep)},r.onAnimationProgress=o.onProgress,r.onAnimationComplete=o.onComplete,t.animationService.addAnimation(a,r,n,i)}else a.draw(),o&&o.onComplete&&o.onComplete.call&&o.onComplete.call(a);return a},draw:function(n){var i=this,a=n||1;i.clear(),t.plugins.notify("beforeDraw",[i,a]),e.each(i.boxes,function(t){t.draw(i.chartArea)},i),i.scale&&i.scale.draw(),t.plugins.notify("beforeDatasetsDraw",[i,a]),e.each(i.data.datasets,function(t,e){i.isDatasetVisible(e)&&i.getDatasetMeta(e).controller.draw(n)},i,!0),t.plugins.notify("afterDatasetsDraw",[i,a]),i.tooltip.transition(a).draw(),t.plugins.notify("afterDraw",[i,a])},getElementAtEvent:function(t){var n=this,i=e.getRelativePosition(t,n.chart),a=[];return e.each(n.data.datasets,function(t,o){if(n.isDatasetVisible(o)){var r=n.getDatasetMeta(o);e.each(r.data,function(t){return t.inRange(i.x,i.y)?(a.push(t),a):void 0})}}),a.slice(0,1)},getElementsAtEvent:function(t){var n=this,i=e.getRelativePosition(t,n.chart),a=[],o=function(){if(n.data.datasets)for(var t=0;t0&&(e=this.getDatasetMeta(e[0]._datasetIndex).data),e},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;n>e;++e)this.isDatasetVisible(e)&&t++;return t},isDatasetVisible:function(t){var e=this.getDatasetMeta(t);return"boolean"==typeof e.hidden?!e.hidden:!this.data.datasets[t].hidden},generateLegend:function(){return this.options.legendCallback(this)},destroy:function(){var n=this;n.stop(),n.clear(),e.unbindEvents(n,n.events),e.removeResizeListener(n.chart.canvas.parentNode);var i=n.chart.canvas;i.width=n.chart.width,i.height=n.chart.height,void 0!==n.chart.originalDevicePixelRatio&&n.chart.ctx.scale(1/n.chart.originalDevicePixelRatio,1/n.chart.originalDevicePixelRatio),i.style.width=n.chart.originalCanvasStyleWidth,i.style.height=n.chart.originalCanvasStyleHeight,t.plugins.notify("destroy",[n]),delete t.instances[n.id]},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)},initToolTip:function(){var e=this;e.tooltip=new t.Tooltip({_chart:e.chart,_chartInstance:e,_data:e.data,_options:e.options.tooltips},e)},bindEvents:function(){var t=this;e.bindEvents(t,t.options.events,function(e){t.eventHandler(e)})},updateHoverStyle:function(t,e,n){var i,a,o,r=n?"setHoverStyle":"removeHoverStyle";switch(e){case"single":t=[t[0]];break;case"label":case"dataset":case"x-axis":break;default:return}for(a=0,o=t.length;o>a;++a)i=t[a],i&&this.getDatasetMeta(i._datasetIndex).controller[r](i)},eventHandler:function(t){var n=this,i=n.tooltip,a=n.options||{},o=a.hover,r=a.tooltips;return n.lastActive=n.lastActive||[],n.lastTooltipActive=n.lastTooltipActive||[],"mouseout"===t.type?(n.active=[],n.tooltipActive=[]):(n.active=n.getElementsAtEventForMode(t,o.mode),n.tooltipActive=n.getElementsAtEventForMode(t,r.mode)),o.onHover&&o.onHover.call(n,n.active),("mouseup"===t.type||"click"===t.type)&&(a.onClick&&a.onClick.call(n,t,n.active),n.legend&&n.legend.handleEvent&&n.legend.handleEvent(t)),n.lastActive.length&&n.updateHoverStyle(n.lastActive,o.mode,!1),n.active.length&&o.mode&&n.updateHoverStyle(n.active,o.mode,!0),(r.enabled||r.custom)&&(i.initialize(),i._active=n.tooltipActive,i.update(!0)),i.pivot(),n.animating||e.arrayEquals(n.active,n.lastActive)&&e.arrayEquals(n.tooltipActive,n.lastTooltipActive)||(n.stop(),(r.enabled||r.custom)&&i.update(!0),n.render(o.animationDuration,!0)),n.lastActive=n.active,n.lastTooltipActive=n.tooltipActive,n}})}},{}],24:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=e.noop;t.DatasetController=function(t,e){this.initialize.call(this,t,e)},e.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)},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||[],o=i.data;for(t=0,e=a.length;e>t;++t)o[t]=o[t]||n.createMetaData(i,t);i.dataset=i.dataset||n.createMetaDataset()},addElementAndReset:function(t){var e=this,n=e.createMetaData(t);e.getMeta().data.splice(t,0,n),e.updateElement(n,t,!0)},buildOrUpdateElements:function(){var t=this.getMeta(),e=t.data,n=this.getDataset().data.length,i=e.length;if(i>n)e.splice(n,i-n);else if(n>i)for(var a=i;n>a;++a)this.addElementAndReset(a)},update:n,draw:function(t){var n=t||1;e.each(this.getMeta().data,function(t){t.transition(n).draw()})},removeHoverStyle:function(t,n){var i=this.chart.data.datasets[t._datasetIndex],a=t._index,o=t.custom||{},r=e.getValueAtIndexOrDefault,s=t._model;s.backgroundColor=o.backgroundColor?o.backgroundColor:r(i.backgroundColor,a,n.backgroundColor),s.borderColor=o.borderColor?o.borderColor:r(i.borderColor,a,n.borderColor),s.borderWidth=o.borderWidth?o.borderWidth:r(i.borderWidth,a,n.borderWidth)},setHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t._index,a=t.custom||{},o=e.getValueAtIndexOrDefault,r=e.getHoverColor,s=t._model;s.backgroundColor=a.hoverBackgroundColor?a.hoverBackgroundColor:o(n.hoverBackgroundColor,i,r(s.backgroundColor)),s.borderColor=a.hoverBorderColor?a.hoverBorderColor:o(n.hoverBorderColor,i,r(s.borderColor)),s.borderWidth=a.hoverBorderWidth?a.hoverBorderWidth:o(n.hoverBorderWidth,i,s.borderWidth)}}),t.DatasetController.extend=e.inherits}},{}],25:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.elements={},t.Element=function(t){e.extend(this,t),this.initialize.apply(this,arguments)},e.extend(t.Element.prototype,{initialize:function(){this.hidden=!1},pivot:function(){var t=this;return t._view||(t._view=e.clone(t._model)),t._start=e.clone(t._view),t},transition:function(t){var n=this;return n._view||(n._view=e.clone(n._model)),1===t?(n._view=n._model,n._start=null,n):(n._start||n.pivot(),e.each(n._model,function(i,a){if("_"===a[0]);else if(n._view.hasOwnProperty(a))if(i===n._view[a]);else if("string"==typeof i)try{var o=e.color(n._model[a]).mix(e.color(n._start[a]),t);n._view[a]=o.rgbString()}catch(r){n._view[a]=i}else if("number"==typeof i){var s=void 0!==n._start[a]&&isNaN(n._start[a])===!1?n._start[a]:0;n._view[a]=(n._model[a]-s)*t+s}else n._view[a]=i;else"number"!=typeof i||isNaN(n._view[a])?n._view[a]=i:n._view[a]=i*t},n),n)},tooltipPosition:function(){return{x:this._model.x,y:this._model.y}},hasValue:function(){return e.isNumber(this._model.x)&&e.isNumber(this._model.y)}}),t.Element.extend=e.inherits}},{}],26:[function(t,e,n){"use strict";var i=t(2);e.exports=function(t){function e(t,e,n){var i;return"string"==typeof t?(i=parseInt(t,10),-1!=t.indexOf("%")&&(i=i/100*e.parentNode[n])):i=t,i}function n(t){return void 0!==t&&null!==t&&"none"!==t}function a(t,i,a){var o=document.defaultView,r=t.parentNode,s=o.getComputedStyle(t)[i],l=o.getComputedStyle(r)[i],d=n(s),u=n(l),c=Number.POSITIVE_INFINITY;return d||u?Math.min(d?e(s,t,a):c,u?e(l,r,a):c):"none"}var o=t.helpers={};o.each=function(t,e,n,i){var a,r;if(o.isArray(t))if(r=t.length,i)for(a=r-1;a>=0;a--)e.call(n,t[a],a);else for(a=0;r>a;a++)e.call(n,t[a],a);else if("object"==typeof t){var s=Object.keys(t);for(r=s.length,a=0;r>a;a++)e.call(n,t[s[a]],s[a])}},o.clone=function(t){var e={};return o.each(t,function(t,n){o.isArray(t)?e[n]=t.slice(0):"object"==typeof t&&null!==t?e[n]=o.clone(t):e[n]=t}),e},o.extend=function(t){for(var e=function(e,n){t[n]=e},n=1,i=arguments.length;i>n;n++)o.each(arguments[n],e);return t},o.configMerge=function(e){var n=o.clone(e);return o.each(Array.prototype.slice.call(arguments,1),function(e){o.each(e,function(e,i){if("scales"===i)n[i]=o.scaleMerge(n.hasOwnProperty(i)?n[i]:{},e);else if("scale"===i)n[i]=o.configMerge(n.hasOwnProperty(i)?n[i]:{},t.scaleService.getScaleDefaults(e.type),e);else if(n.hasOwnProperty(i)&&o.isArray(n[i])&&o.isArray(e)){var a=n[i];o.each(e,function(t,e){e=i[n].length||!i[n][a].type?i[n].push(o.configMerge(s,e)):e.type&&e.type!==i[n][a].type?i[n][a]=o.configMerge(i[n][a],s,e):i[n][a]=o.configMerge(i[n][a],e)}):(i[n]=[],o.each(e,function(e){var a=o.getValueOrDefault(e.type,"xAxes"===n?"category":"linear");i[n].push(o.configMerge(t.scaleService.getScaleDefaults(a),e))})):i.hasOwnProperty(n)&&"object"==typeof i[n]&&null!==i[n]&&"object"==typeof e?i[n]=o.configMerge(i[n],e):i[n]=e}),i},o.getValueAtIndexOrDefault=function(t,e,n){return void 0===t||null===t?n:o.isArray(t)?en;++n)if(t[n]===e)return n;return-1},o.where=function(t,e){if(o.isArray(t)&&Array.prototype.filter)return t.filter(e);var n=[];return o.each(t,function(t){e(t)&&n.push(t)}),n},o.findIndex=Array.prototype.findIndex?function(t,e,n){return t.findIndex(e,n)}:function(t,e,n){n=void 0===n?t:n;for(var i=0,a=t.length;a>i;++i)if(e.call(n,t[i],i,t))return i;return-1},o.findNextWhere=function(t,e,n){(void 0===n||null===n)&&(n=-1);for(var i=n+1;i=0;i--){var a=t[i];if(e(a))return a}},o.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=o.inherits,t&&o.extend(n.prototype,t),n.__super__=e.prototype,n},o.noop=function(){},o.uid=function(){var t=0;return function(){return t++}}(),o.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},o.almostEquals=function(t,e,n){return Math.abs(t-e)0?1:-1},o.log10=Math.log10?function(t){return Math.log10(t)}:function(t){return Math.log(t)/Math.LN10},o.toRadians=function(t){return t*(Math.PI/180)},o.toDegrees=function(t){return t*(180/Math.PI)},o.getAngleFromPoint=function(t,e){var n=e.x-t.x,i=e.y-t.y,a=Math.sqrt(n*n+i*i),o=Math.atan2(i,n);return o<-.5*Math.PI&&(o+=2*Math.PI),{angle:o,distance:a}},o.aliasPixel=function(t){return t%2===0?0:.5},o.splineCurve=function(t,e,n,i){var a=t.skip?e:t,o=e,r=n.skip?e:n,s=Math.sqrt(Math.pow(o.x-a.x,2)+Math.pow(o.y-a.y,2)),l=Math.sqrt(Math.pow(r.x-o.x,2)+Math.pow(r.y-o.y,2)),d=s/(s+l),u=l/(s+l);d=isNaN(d)?0:d,u=isNaN(u)?0:u;var c=i*d,h=i*u;return{previous:{x:o.x-c*(r.x-a.x),y:o.y-c*(r.y-a.y)},next:{x:o.x+h*(r.x-a.x),y:o.y+h*(r.y-a.y)}}},o.EPSILON=Number.EPSILON||1e-14,o.splineCurveMonotone=function(t){var e,n,i,a,r=(t||[]).map(function(t){return{model:t._model,deltaK:0,mK:0}}),s=r.length;for(e=0;s>e;++e)i=r[e],i.model.skip||(n=e>0?r[e-1]:null,a=s-1>e?r[e+1]:null,a&&!a.model.skip&&(i.deltaK=(a.model.y-i.model.y)/(a.model.x-i.model.x)),!n||n.model.skip?i.mK=i.deltaK:!a||a.model.skip?i.mK=n.deltaK:this.sign(n.deltaK)!=this.sign(i.deltaK)?i.mK=0:i.mK=(n.deltaK+i.deltaK)/2);var l,d,u,c;for(e=0;s-1>e;++e)i=r[e],a=r[e+1],i.model.skip||a.model.skip||(o.almostEquals(i.deltaK,0,this.EPSILON)?i.mK=a.mK=0:(l=i.mK/i.deltaK,d=a.mK/i.deltaK,c=Math.pow(l,2)+Math.pow(d,2),9>=c||(u=3/Math.sqrt(c),i.mK=l*u*i.deltaK,a.mK=d*u*i.deltaK)));var h;for(e=0;s>e;++e)i=r[e],i.model.skip||(n=e>0?r[e-1]:null,a=s-1>e?r[e+1]:null,n&&!n.model.skip&&(h=(i.model.x-n.model.x)/3,i.model.controlPointPreviousX=i.model.x-h,i.model.controlPointPreviousY=i.model.y-h*i.mK),a&&!a.model.skip&&(h=(a.model.x-i.model.x)/3,i.model.controlPointNextX=i.model.x+h,i.model.controlPointNextY=i.model.y+h*i.mK))},o.nextItem=function(t,e,n){return n?e>=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},o.previousItem=function(t,e,n){return n?0>=e?t[t.length-1]:t[e-1]:0>=e?t[0]:t[e-1]},o.niceNum=function(t,e){var n,i=Math.floor(o.log10(t)),a=t/Math.pow(10,i);return n=e?1.5>a?1:3>a?2:7>a?5:10:1>=a?1:2>=a?2:5>=a?5:10,n*Math.pow(10,i)};var r=o.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:-0.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:-0.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-0.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?-0.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),it?-.5*(i*Math.pow(2,10*(t-=1))*Math.sin((1*t-e)*(2*Math.PI)/n)):i*Math.pow(2,-10*(t-=1))*Math.sin((1*t-e)*(2*Math.PI)/n)*.5+1)},easeInBack:function(t){var e=1.70158;return 1*(t/=1)*t*((e+1)*t-e)},easeOutBack:function(t){var e=1.70158;return 1*((t=t/1-1)*t*((e+1)*t+e)+1)},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?.5*(t*t*(((e*=1.525)+1)*t-e)):.5*((t-=2)*t*(((e*=1.525)+1)*t+e)+2)},easeInBounce:function(t){return 1-r.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?1*(7.5625*t*t):2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*r.easeInBounce(2*t):.5*r.easeOutBounce(2*t-1)+.5}};o.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),o.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),o.getRelativePosition=function(t,e){var n,i,a=t.originalEvent||t,r=t.currentTarget||t.srcElement,s=r.getBoundingClientRect(),l=a.touches;l&&l.length>0?(n=l[0].clientX,i=l[0].clientY):(n=a.clientX,i=a.clientY);var d=parseFloat(o.getStyle(r,"padding-left")),u=parseFloat(o.getStyle(r,"padding-top")),c=parseFloat(o.getStyle(r,"padding-right")),h=parseFloat(o.getStyle(r,"padding-bottom")),f=s.right-s.left-d-c,g=s.bottom-s.top-u-h;return n=Math.round((n-s.left-d)/f*r.width/e.currentDevicePixelRatio), -i=Math.round((i-s.top-u)/g*r.height/e.currentDevicePixelRatio),{x:n,y:i}},o.addEvent=function(t,e,n){t.addEventListener?t.addEventListener(e,n):t.attachEvent?t.attachEvent("on"+e,n):t["on"+e]=n},o.removeEvent=function(t,e,n){t.removeEventListener?t.removeEventListener(e,n,!1):t.detachEvent?t.detachEvent("on"+e,n):t["on"+e]=o.noop},o.bindEvents=function(t,e,n){var i=t.events=t.events||{};o.each(e,function(e){i[e]=function(){n.apply(t,arguments)},o.addEvent(t.chart.canvas,e,i[e])})},o.unbindEvents=function(t,e){var n=t.chart.canvas;o.each(e,function(t,e){o.removeEvent(n,e,t)})},o.getConstraintWidth=function(t){return a(t,"max-width","clientWidth")},o.getConstraintHeight=function(t){return a(t,"max-height","clientHeight")},o.getMaximumWidth=function(t){var e=t.parentNode,n=parseInt(o.getStyle(e,"padding-left"))+parseInt(o.getStyle(e,"padding-right")),i=e.clientWidth-n,a=o.getConstraintWidth(t);return isNaN(a)?i:Math.min(i,a)},o.getMaximumHeight=function(t){var e=t.parentNode,n=parseInt(o.getStyle(e,"padding-top"))+parseInt(o.getStyle(e,"padding-bottom")),i=e.clientHeight-n,a=o.getConstraintHeight(t);return isNaN(a)?i:Math.min(i,a)},o.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},o.retinaScale=function(t){var e=t.ctx,n=t.canvas,i=n.width,a=n.height,o=t.currentDevicePixelRatio=window.devicePixelRatio||1;1!==o&&(n.height=a*o,n.width=i*o,e.scale(o,o),t.originalDevicePixelRatio=t.originalDevicePixelRatio||o),n.style.width=i+"px",n.style.height=a+"px"},o.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},o.fontString=function(t,e,n){return e+" "+t+"px "+n},o.longestText=function(t,e,n,i){i=i||{};var a=i.data=i.data||{},r=i.garbageCollect=i.garbageCollect||[];i.font!==e&&(a=i.data={},r=i.garbageCollect=[],i.font=e),t.font=e;var s=0;o.each(n,function(e){void 0!==e&&null!==e&&o.isArray(e)!==!0?s=o.measureText(t,a,r,s,e):o.isArray(e)&&o.each(e,function(e){void 0===e||null===e||o.isArray(e)||(s=o.measureText(t,a,r,s,e))})});var l=r.length/2;if(l>n.length){for(var d=0;l>d;d++)delete a[r[d]];r.splice(0,l)}return s},o.measureText=function(t,e,n,i,a){var o=e[a];return o||(o=e[a]=t.measureText(a).width,n.push(a)),o>i&&(i=o),i},o.numberOfLabelLines=function(t){var e=1;return o.each(t,function(t){o.isArray(t)&&t.length>e&&(e=t.length)}),e},o.drawRoundedRectangle=function(t,e,n,i,a,o){t.beginPath(),t.moveTo(e+o,n),t.lineTo(e+i-o,n),t.quadraticCurveTo(e+i,n,e+i,n+o),t.lineTo(e+i,n+a-o),t.quadraticCurveTo(e+i,n+a,e+i-o,n+a),t.lineTo(e+o,n+a),t.quadraticCurveTo(e,n+a,e,n+a-o),t.lineTo(e,n+o),t.quadraticCurveTo(e,n,e+o,n),t.closePath()},o.color=function(e){return i?i(e instanceof CanvasGradient?t.defaults.global.defaultColor:e):(console.log("Color.js not found!"),e)},o.addResizeListener=function(t,e){var n=document.createElement("iframe"),i="chartjs-hidden-iframe";n.classlist?n.classlist.add(i):n.setAttribute("class",i),n.tabIndex=-1;var a=n.style;a.width="100%",a.display="block",a.border=0,a.height=0,a.margin=0,a.position="absolute",a.left=0,a.right=0,a.top=0,a.bottom=0,t.insertBefore(n,t.firstChild),(n.contentWindow||n).onresize=function(){e&&e()}},o.removeResizeListener=function(t){var e=t.querySelector(".chartjs-hidden-iframe");e&&e.parentNode.removeChild(e)},o.isArray=Array.isArray?function(t){return Array.isArray(t)}:function(t){return"[object Array]"===Object.prototype.toString.call(t)},o.arrayEquals=function(t,e){var n,i,a,r;if(!t||!e||t.length!=e.length)return!1;for(n=0,i=t.length;i>n;++n)if(a=t[n],r=e[n],a instanceof Array&&r instanceof Array){if(!o.arrayEquals(a,r))return!1}else if(a!=r)return!1;return!0},o.callCallback=function(t,e,n){t&&"function"==typeof t.call&&t.apply(n,e)},o.getHoverColor=function(t){return t instanceof CanvasPattern?t:o.color(t).saturate(.5).darken(.1).rgbString()}}},{2:2}],27:[function(t,e,n){"use strict";e.exports=function(){var t=function(e,n){var i=this,a=t.helpers;return i.config=n||{data:{datasets:[]}},e.length&&e[0].getContext&&(e=e[0]),e.getContext&&(e=e.getContext("2d")),i.ctx=e,i.canvas=e.canvas,e.canvas.style.display=e.canvas.style.display||"block",i.width=e.canvas.width||parseInt(a.getStyle(e.canvas,"width"),10)||a.getMaximumWidth(e.canvas),i.height=e.canvas.height||parseInt(a.getStyle(e.canvas,"height"),10)||a.getMaximumHeight(e.canvas),i.aspectRatio=i.width/i.height,(isNaN(i.aspectRatio)||isFinite(i.aspectRatio)===!1)&&(i.aspectRatio=void 0!==n.aspectRatio?n.aspectRatio:2),i.originalCanvasStyleWidth=e.canvas.style.width,i.originalCanvasStyleHeight=e.canvas.style.height,a.retinaScale(i),i.controller=new t.Controller(i),a.addResizeListener(e.canvas.parentNode,function(){i.controller&&i.controller.config.options.responsive&&i.controller.resize()}),i.controller?i.controller:i};return t.defaults={global:{responsive:!0,responsiveAnimationDuration:0,maintainAspectRatio:!0,events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"single",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}},{}],28:[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?m:k,x),S-=e.height):(e=t.update(y,b),k-=e.width),w.push({horizontal:n,minSize:e,box:t})}function o(t){var n=e.findNextWhere(w,function(e){return e.box===t});if(n)if(t.isHorizontal()){var i={left:_,right:M,top:0,bottom:0};t.update(t.options.fullWidth?m:k,p/2,i)}else t.update(n.minSize.width,S)}function r(t){var n=e.findNextWhere(w,function(e){return e.box===t}),i={left:0,right:0,top:D,bottom:C};n&&t.update(n.minSize.width,S,i)}function s(t){t.isHorizontal()?(t.left=t.options.fullWidth?l:_,t.right=t.options.fullWidth?n-l:_+k,t.top=F,t.bottom=F+t.height,F=t.bottom):(t.left=A,t.right=A+t.width,t.top=D,t.bottom=D+S,A=t.right)}if(t){var l=0,d=0,u=e.where(t.boxes,function(t){return"left"===t.options.position}),c=e.where(t.boxes,function(t){return"right"===t.options.position}),h=e.where(t.boxes,function(t){return"top"===t.options.position}),f=e.where(t.boxes,function(t){return"bottom"===t.options.position}),g=e.where(t.boxes,function(t){return"chartArea"===t.options.position});h.sort(function(t,e){return(e.options.fullWidth?1:0)-(t.options.fullWidth?1:0)}),f.sort(function(t,e){return(t.options.fullWidth?1:0)-(e.options.fullWidth?1:0)});var m=n-2*l,p=i-2*d,v=m/2,b=p/2,y=(n-v)/(u.length+c.length),x=(i-b)/(h.length+f.length),k=m,S=p,w=[];e.each(u.concat(c,h,f),a);var _=l,M=l,D=d,C=d;e.each(u.concat(c),o),e.each(u,function(t){_+=t.width}),e.each(c,function(t){M+=t.width}),e.each(h.concat(f),o),e.each(h,function(t){D+=t.height}),e.each(f,function(t){C+=t.height}),e.each(u.concat(c),r),_=l,M=l,D=d,C=d,e.each(u,function(t){_+=t.width}),e.each(c,function(t){M+=t.width}),e.each(h,function(t){D+=t.height}),e.each(f,function(t){C+=t.height});var T=i-D-C,P=n-_-M;(P!==k||T!==S)&&(e.each(u,function(t){t.height=T}),e.each(c,function(t){t.height=T}),e.each(h,function(t){t.options.fullWidth||(t.width=P)}),e.each(f,function(t){t.options.fullWidth||(t.width=P)}),S=T,k=P);var A=l,F=d;e.each(u.concat(h),s),A+=k,F+=S,e.each(c,s),e.each(f,s),t.chartArea={left:_,top:D,right:_+k,bottom:D+S},e.each(g,function(e){e.left=t.chartArea.left,e.top=t.chartArea.top,e.right=t.chartArea.right,e.bottom=t.chartArea.bottom,e.update(k,S)})}}}}},{}],29:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=e.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()},labels:{boxWidth:40,padding:10,generateLabels:function(t){var n=t.data;return e.isArray(n.datasets)?n.datasets.map(function(n,i){return{text:n.label,fillStyle:e.isArray(n.backgroundColor)?n.backgroundColor[0]:n.backgroundColor,hidden:!t.isDatasetVisible(i),lineCap:n.borderCapStyle,lineDash:n.borderDash,lineDashOffset:n.borderDashOffset,lineJoin:n.borderJoinStyle,lineWidth:n.borderWidth,strokeStyle:n.borderColor,pointStyle:n.pointStyle,datasetIndex:i}},this):[]}}},t.Legend=t.Element.extend({initialize:function(t){e.extend(this,t),this.legendHitBoxes=[],this.doughnutMode=!1},beforeUpdate:n,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:n,beforeSetDimensions:n,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:n,beforeBuildLabels:n,buildLabels:function(){var t=this;t.legendItems=t.options.labels.generateLabels.call(t,t.chart),t.options.reverse&&t.legendItems.reverse()},afterBuildLabels:n,beforeFit:n,fit:function(){var n=this,i=n.options,a=i.labels,o=i.display,r=n.ctx,s=t.defaults.global,l=e.getValueOrDefault,d=l(a.fontSize,s.defaultFontSize),u=l(a.fontStyle,s.defaultFontStyle),c=l(a.fontFamily,s.defaultFontFamily),h=e.fontString(d,u,c),f=n.legendHitBoxes=[],g=n.minSize,m=n.isHorizontal();if(m?(g.width=n.maxWidth,g.height=o?10:0):(g.width=o?10:0,g.height=n.maxHeight),o)if(r.font=h,m){var p=n.lineWidths=[0],v=n.legendItems.length?d+a.padding:0;r.textAlign="left",r.textBaseline="top",e.each(n.legendItems,function(t,e){var i=a.usePointStyle?d*Math.sqrt(2):a.boxWidth,o=i+d/2+r.measureText(t.text).width;p[p.length-1]+o+a.padding>=n.width&&(v+=d+a.padding,p[p.length]=n.left),f[e]={left:0,top:0,width:o,height:d},p[p.length-1]+=o+a.padding}),g.height+=v}else{var b=a.padding,y=n.columnWidths=[],x=a.padding,k=0,S=0,w=d+b;e.each(n.legendItems,function(t,e){var n=a.usePointStyle?2*a.boxWidth:a.boxWidth,i=n+d/2+r.measureText(t.text).width;S+w>g.height&&(x+=k+a.padding,y.push(k),k=0,S=0),k=Math.max(k,i),S+=w,f[e]={left:0,top:0,width:i,height:d}}),x+=k,y.push(k),g.width+=x}n.width=g.width,n.height=g.height},afterFit:n,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var n=this,i=n.options,a=i.labels,o=t.defaults.global,r=o.elements.line,s=n.width,l=n.lineWidths;if(i.display){var d,u=n.ctx,c=e.getValueOrDefault,h=c(a.fontColor,o.defaultFontColor),f=c(a.fontSize,o.defaultFontSize),g=c(a.fontStyle,o.defaultFontStyle),m=c(a.fontFamily,o.defaultFontFamily),p=e.fontString(f,g,m);u.textAlign="left",u.textBaseline="top",u.lineWidth=.5,u.strokeStyle=h,u.fillStyle=h,u.font=p;var v=a.boxWidth,b=n.legendHitBoxes,y=function(e,n,a){if(!(isNaN(v)||0>=v)){if(u.save(),u.fillStyle=c(a.fillStyle,o.defaultColor),u.lineCap=c(a.lineCap,r.borderCapStyle),u.lineDashOffset=c(a.lineDashOffset,r.borderDashOffset),u.lineJoin=c(a.lineJoin,r.borderJoinStyle),u.lineWidth=c(a.lineWidth,r.borderWidth),u.strokeStyle=c(a.strokeStyle,o.defaultColor),u.setLineDash&&u.setLineDash(c(a.lineDash,r.borderDash)),i.labels&&i.labels.usePointStyle){var s=f*Math.SQRT2/2,l=s/Math.SQRT2,d=e+l,h=n+l;t.canvasHelpers.drawPoint(u,a.pointStyle,s,d,h)}else u.strokeRect(e,n,v,f),u.fillRect(e,n,v,f);u.restore()}},x=function(t,e,n,i){u.fillText(n.text,v+f/2+t,e),n.hidden&&(u.beginPath(),u.lineWidth=2,u.moveTo(v+f/2+t,e+f/2),u.lineTo(v+f/2+t+i,e+f/2),u.stroke())},k=n.isHorizontal();d=k?{x:n.left+(s-l[0])/2,y:n.top+a.padding,line:0}:{x:n.left+a.padding,y:n.top+a.padding,line:0};var S=f+a.padding;e.each(n.legendItems,function(t,e){var i=u.measureText(t.text).width,o=a.usePointStyle?f+f/2+i:v+f/2+i,r=d.x,c=d.y;k?r+o>=s&&(c=d.y+=S,d.line++,r=d.x=n.left+(s-l[d.line])/2):c+S>n.bottom&&(r=d.x=r+n.columnWidths[d.line]+a.padding,c=d.y=n.top,d.line++),y(r,c,t),b[e].left=r,b[e].top=c,x(r,c,t,i),k?d.x+=o+a.padding:d.y+=S})}},handleEvent:function(t){var n=this,i=e.getRelativePosition(t,n.chart.chart),a=i.x,o=i.y,r=n.options;if(a>=n.left&&a<=n.right&&o>=n.top&&o<=n.bottom)for(var s=n.legendHitBoxes,l=0;l=d.left&&a<=d.left+d.width&&o>=d.top&&o<=d.top+d.height){r.onClick&&r.onClick.call(n,t,n.legendItems[l]);break}}}}),t.plugins.register({beforeInit:function(e){var n=e.options,i=n.legend;i&&(e.legend=new t.Legend({ctx:e.chart.ctx,options:i,chart:e}),t.layoutService.addBox(e,e.legend))}})}},{}],30:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers.noop;t.plugins={_plugins:[],register:function(t){var e=this._plugins;[].concat(t).forEach(function(t){-1===e.indexOf(t)&&e.push(t)})},unregister:function(t){var e=this._plugins;[].concat(t).forEach(function(t){var n=e.indexOf(t);-1!==n&&e.splice(n,1)})},clear:function(){this._plugins=[]},count:function(){return this._plugins.length},getAll:function(){return this._plugins},notify:function(t,e){var n,i,a=this._plugins,o=a.length;for(n=0;o>n;++n)if(i=a[n],"function"==typeof i[t]&&i[t].apply(i,e||[])===!1)return!1;return!0}},t.PluginBase=t.Element.extend({beforeInit:e,afterInit:e,beforeUpdate:e,afterUpdate:e,beforeDraw:e,afterDraw:e,destroy:e}),t.pluginService=t.plugins}},{}],31:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.scale={display:!0,position:"left",gridLines:{display:!0,color:"rgba(0, 0, 0, 0.1)",lineWidth:1,drawBorder:!0,drawOnChartArea:!0,drawTicks:!0,tickMarkLength:10,zeroLineWidth:1,zeroLineColor:"rgba(0,0,0,0.25)",offsetGridLines:!1,borderDash:[],borderDashOffset:0},scaleLabel:{labelString:"",display:!1},ticks:{beginAtZero:!1,minRotation:0,maxRotation:50,mirror:!1,padding:10,reverse:!1,display:!0,autoSkip:!0,autoSkipPadding:0,labelOffset:0,callback:function(t){return e.isArray(t)?t:""+t}}},t.Scale=t.Element.extend({beforeUpdate:function(){e.callCallback(this.options.beforeUpdate,[this])},update:function(t,n,i){var a=this;return a.beforeUpdate(),a.maxWidth=t,a.maxHeight=n,a.margins=e.extend({left:0,right:0,top:0,bottom:0},i),a.beforeSetDimensions(),a.setDimensions(),a.afterSetDimensions(),a.beforeDataLimits(),a.determineDataLimits(),a.afterDataLimits(),a.beforeBuildTicks(),a.buildTicks(),a.afterBuildTicks(),a.beforeTickToLabelConversion(),a.convertTicksToLabels(),a.afterTickToLabelConversion(),a.beforeCalculateTickRotation(),a.calculateTickRotation(),a.afterCalculateTickRotation(),a.beforeFit(),a.fit(),a.afterFit(),a.afterUpdate(),a.minSize},afterUpdate:function(){e.callCallback(this.options.afterUpdate,[this])},beforeSetDimensions:function(){e.callCallback(this.options.beforeSetDimensions,[this])},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},afterSetDimensions:function(){e.callCallback(this.options.afterSetDimensions,[this])},beforeDataLimits:function(){e.callCallback(this.options.beforeDataLimits,[this])},determineDataLimits:e.noop,afterDataLimits:function(){e.callCallback(this.options.afterDataLimits,[this])},beforeBuildTicks:function(){e.callCallback(this.options.beforeBuildTicks,[this])},buildTicks:e.noop,afterBuildTicks:function(){e.callCallback(this.options.afterBuildTicks,[this])},beforeTickToLabelConversion:function(){e.callCallback(this.options.beforeTickToLabelConversion,[this])},convertTicksToLabels:function(){var t=this;t.ticks=t.ticks.map(function(e,n,i){return t.options.ticks.userCallback?t.options.ticks.userCallback(e,n,i):t.options.ticks.callback(e,n,i)},t)},afterTickToLabelConversion:function(){e.callCallback(this.options.afterTickToLabelConversion,[this])},beforeCalculateTickRotation:function(){e.callCallback(this.options.beforeCalculateTickRotation,[this])},calculateTickRotation:function(){var n=this,i=n.ctx,a=t.defaults.global,o=n.options.ticks,r=e.getValueOrDefault(o.fontSize,a.defaultFontSize),s=e.getValueOrDefault(o.fontStyle,a.defaultFontStyle),l=e.getValueOrDefault(o.fontFamily,a.defaultFontFamily),d=e.fontString(r,s,l);i.font=d;var u,c=i.measureText(n.ticks[0]).width,h=i.measureText(n.ticks[n.ticks.length-1]).width;if(n.labelRotation=o.minRotation||0,n.paddingRight=0,n.paddingLeft=0,n.options.display&&n.isHorizontal()){n.paddingRight=h/2+3,n.paddingLeft=c/2+3,n.longestTextCache||(n.longestTextCache={});for(var f,g,m=e.longestText(i,d,n.ticks,n.longestTextCache),p=m,v=n.getPixelForTick(1)-n.getPixelForTick(0)-6;p>v&&n.labelRotationn.yLabelWidth&&(n.paddingLeft=u+r/2),n.paddingRight=r/2,g*m>n.maxHeight){n.labelRotation--;break}n.labelRotation++,p=f*m}}n.margins&&(n.paddingLeft=Math.max(n.paddingLeft-n.margins.left,0),n.paddingRight=Math.max(n.paddingRight-n.margins.right,0))},afterCalculateTickRotation:function(){e.callCallback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){e.callCallback(this.options.beforeFit,[this])},fit:function(){var n=this,i=n.minSize={width:0,height:0},a=n.options,o=t.defaults.global,r=a.ticks,s=a.scaleLabel,l=a.gridLines,d=a.display,u=n.isHorizontal(),c=e.getValueOrDefault(r.fontSize,o.defaultFontSize),h=e.getValueOrDefault(r.fontStyle,o.defaultFontStyle),f=e.getValueOrDefault(r.fontFamily,o.defaultFontFamily),g=e.fontString(c,h,f),m=e.getValueOrDefault(s.fontSize,o.defaultFontSize),p=a.gridLines.tickMarkLength;if(u?i.width=n.isFullWidth()?n.maxWidth-n.margins.left-n.margins.right:n.maxWidth:i.width=d&&l.drawTicks?p:0,u?i.height=d&&l.drawTicks?p:0:i.height=n.maxHeight,s.display&&d&&(u?i.height+=1.5*m:i.width+=1.5*m),r.display&&d){n.longestTextCache||(n.longestTextCache={});var v=e.longestText(n.ctx,g,n.ticks,n.longestTextCache),b=e.numberOfLabelLines(n.ticks),y=.5*c;if(u){n.longestLabelWidth=v;var x=Math.sin(e.toRadians(n.labelRotation))*n.longestLabelWidth+c*b+y*b;i.height=Math.min(n.maxHeight,i.height+x),n.ctx.font=g;var k=n.ctx.measureText(n.ticks[0]).width,S=n.ctx.measureText(n.ticks[n.ticks.length-1]).width,w=Math.cos(e.toRadians(n.labelRotation)),_=Math.sin(e.toRadians(n.labelRotation));n.paddingLeft=0!==n.labelRotation?w*k+3:k/2+3,n.paddingRight=0!==n.labelRotation?_*(c/2)+3:S/2+3}else{var M=n.maxWidth-i.width,D=r.mirror;D?v=0:v+=n.options.ticks.padding,M>v?i.width+=v:i.width=n.maxWidth,n.paddingTop=c/2,n.paddingBottom=c/2}}n.margins&&(n.paddingLeft=Math.max(n.paddingLeft-n.margins.left,0),n.paddingTop=Math.max(n.paddingTop-n.margins.top,0),n.paddingRight=Math.max(n.paddingRight-n.margins.right,0),n.paddingBottom=Math.max(n.paddingBottom-n.margins.bottom,0)),n.width=i.width,n.height=i.height},afterFit:function(){e.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&&isNaN(t)?NaN:"object"==typeof t?t instanceof Date||t.isValid?t:this.getRightValue(this.isHorizontal()?t.x:t.y):t},getLabelForIndex:e.noop,getPixelForValue:e.noop,getValueForPixel:e.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),o=a*t+n.paddingLeft;e&&(o+=a/2);var r=n.left+Math.round(o);return r+=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(){var t=this,e=t.min,n=t.max;return t.getPixelForValue(t.beginAtZero?0:0>e&&0>n?n:e>0&&n>0?e:0)},draw:function(n){var i=this,a=i.options;if(a.display){var o,r,s=i.ctx,l=t.defaults.global,d=a.ticks,u=a.gridLines,c=a.scaleLabel,h=0!==i.labelRotation,f=d.autoSkip,g=i.isHorizontal();d.maxTicksLimit&&(r=d.maxTicksLimit);var m=e.getValueOrDefault(d.fontColor,l.defaultFontColor),p=e.getValueOrDefault(d.fontSize,l.defaultFontSize),v=e.getValueOrDefault(d.fontStyle,l.defaultFontStyle),b=e.getValueOrDefault(d.fontFamily,l.defaultFontFamily),y=e.fontString(p,v,b),x=u.tickMarkLength,k=e.getValueOrDefault(u.borderDash,l.borderDash),S=e.getValueOrDefault(u.borderDashOffset,l.borderDashOffset),w=e.getValueOrDefault(c.fontColor,l.defaultFontColor),_=e.getValueOrDefault(c.fontSize,l.defaultFontSize),M=e.getValueOrDefault(c.fontStyle,l.defaultFontStyle),D=e.getValueOrDefault(c.fontFamily,l.defaultFontFamily),C=e.fontString(_,M,D),T=e.toRadians(i.labelRotation),P=Math.cos(T),A=i.longestLabelWidth*P;s.fillStyle=m;var F=[];if(g){if(o=!1,h&&(A/=2),(A+d.autoSkipPadding)*i.ticks.length>i.width-(i.paddingLeft+i.paddingRight)&&(o=1+Math.floor((A+d.autoSkipPadding)*i.ticks.length/(i.width-(i.paddingLeft+i.paddingRight)))),r&&i.ticks.length>r)for(;!o||i.ticks.length/(o||1)>r;)o||(o=1),o+=1;f||(o=!1)}var I="right"===a.position?i.left:i.right-x,O="right"===a.position?i.left+x:i.right,R="bottom"===a.position?i.top:i.bottom-x,W="bottom"===a.position?i.top+x:i.bottom;if(e.each(i.ticks,function(t,r){if(void 0!==t&&null!==t){var s=i.ticks.length===r+1,l=o>1&&r%o>0||r%o===0&&r+o>=i.ticks.length;if((!l||s)&&void 0!==t&&null!==t){var c,f;r===("undefined"!=typeof i.zeroLineIndex?i.zeroLineIndex:0)?(c=u.zeroLineWidth,f=u.zeroLineColor):(c=e.getValueAtIndexOrDefault(u.lineWidth,r),f=e.getValueAtIndexOrDefault(u.color,r));var m,p,v,b,y,w,_,M,D,C,P,A="middle";if(g){h||(A="top"===a.position?"bottom":"top"),P=h?"right":"center";var L=i.getPixelForTick(r)+e.aliasPixel(c);D=i.getPixelForTick(r,u.offsetGridLines)+d.labelOffset,C=h?i.top+12:"top"===a.position?i.bottom-x:i.top+x,m=v=y=_=L,p=R,b=W,w=n.top,M=n.bottom}else{"left"===a.position?d.mirror?(D=i.right+d.padding,P="left"):(D=i.right-d.padding,P="right"):d.mirror?(D=i.left-d.padding,P="right"):(D=i.left+d.padding,P="left");var V=i.getPixelForTick(r);V+=e.aliasPixel(c),C=i.getPixelForTick(r,u.offsetGridLines),m=I,v=O,y=n.left,_=n.right,p=b=w=M=V}F.push({tx1:m,ty1:p,tx2:v,ty2:b,x1:y,y1:w,x2:_,y2:M,labelX:D,labelY:C,glWidth:c,glColor:f,glBorderDash:k,glBorderDashOffset:S,rotation:-1*T,label:t,textBaseline:A,textAlign:P})}}}),e.each(F,function(t){if(u.display&&(s.save(),s.lineWidth=t.glWidth,s.strokeStyle=t.glColor,s.setLineDash&&(s.setLineDash(t.glBorderDash),s.lineDashOffset=t.glBorderDashOffset),s.beginPath(),u.drawTicks&&(s.moveTo(t.tx1,t.ty1),s.lineTo(t.tx2,t.ty2)),u.drawOnChartArea&&(s.moveTo(t.x1,t.y1),s.lineTo(t.x2,t.y2)),s.stroke(),s.restore()),d.display){s.save(),s.translate(t.labelX,t.labelY),s.rotate(t.rotation),s.font=y,s.textBaseline=t.textBaseline,s.textAlign=t.textAlign;var n=t.label;if(e.isArray(n))for(var i=0,a=0;ie;++e){var o=t[e];if(o&&o.hasValue()){var r=o.tooltipPosition();i.push(r.x),a.push(r.y)}}var s=0,l=0;for(e=0;e0){var o=t[0];o.xLabel?n=o.xLabel:a>0&&o.indexe;++e)g.push(i(d[e]));s.itemSort&&(g=g.sort(function(t,e){return s.itemSort(t,e,u)})),d.length>1&&a.each(g,function(t){h.push(s.callbacks.labelColor.call(r,t,c))}),a.extend(l,{title:r.getTitle(g,u),beforeBody:r.getBeforeBody(g,u),body:r.getBody(g,u),afterBody:r.getAfterBody(g,u),footer:r.getFooter(g,u),x:Math.round(f.x),y:Math.round(f.y),caretPadding:a.getValueOrDefault(f.padding,2),labelColors:h});var m=r.getTooltipSize(l);r.determineAlignment(m),a.extend(l,r.getBackgroundPoint(l,m))}else r._model.opacity=0;return t&&s.custom&&s.custom.call(r,l),r},getTooltipSize:function(t){var e=this._chart.ctx,n={height:2*t.yPadding,width:0},i=t.body,o=i.reduce(function(t,e){return t+e.before.length+e.lines.length+e.after.length},0);o+=t.beforeBody.length+t.afterBody.length;var r=t.title.length,s=t.footer.length,l=t.titleFontSize,d=t.bodyFontSize,u=t.footerFontSize;n.height+=r*l,n.height+=(r-1)*t.titleSpacing,n.height+=r?t.titleMarginBottom:0,n.height+=o*d,n.height+=o?(o-1)*t.bodySpacing:0,n.height+=s?t.footerMarginTop:0,n.height+=s*u, -n.height+=s?(s-1)*t.footerSpacing:0;var c=0,h=function(t){n.width=Math.max(n.width,e.measureText(t).width+c)};return e.font=a.fontString(l,t._titleFontStyle,t._titleFontFamily),a.each(t.title,h),e.font=a.fontString(d,t._bodyFontStyle,t._bodyFontFamily),a.each(t.beforeBody.concat(t.afterBody),h),c=i.length>1?d+2:0,a.each(i,function(t){a.each(t.before,h),a.each(t.lines,h),a.each(t.after,h)}),c=0,e.font=a.fontString(u,t._footerFontStyle,t._footerFontFamily),a.each(t.footer,h),n.width+=2*t.xPadding,n},determineAlignment:function(t){var e=this,n=e._model,i=e._chart,a=e._chartInstance.chartArea;n.yi.height-t.height&&(n.yAlign="bottom");var o,r,s,l,d,u=(a.left+a.right)/2,c=(a.top+a.bottom)/2;"center"===n.yAlign?(o=function(t){return u>=t},r=function(t){return t>u}):(o=function(e){return e<=t.width/2},r=function(e){return e>=i.width-t.width/2}),s=function(e){return e+t.width>i.width},l=function(e){return e-t.width<0},d=function(t){return c>=t?"top":"bottom"},o(n.x)?(n.xAlign="left",s(n.x)&&(n.xAlign="center",n.yAlign=d(n.y))):r(n.x)&&(n.xAlign="right",l(n.x)&&(n.xAlign="center",n.yAlign=d(n.y)))},getBackgroundPoint:function(t,e){var n={x:t.x,y:t.y},i=t.caretSize,a=t.caretPadding,o=t.cornerRadius,r=t.xAlign,s=t.yAlign,l=i+a,d=o+a;return"right"===r?n.x-=e.width:"center"===r&&(n.x-=e.width/2),"top"===s?n.y+=l:"bottom"===s?n.y-=e.height+l:n.y-=e.height/2,"center"===s?"left"===r?n.x+=l:"right"===r&&(n.x-=l):"left"===r?n.x-=d:"right"===r&&(n.x+=d),n},drawCaret:function(t,e,n){var i,o,r,s,l,d,u=this._view,c=this._chart.ctx,h=u.caretSize,f=u.cornerRadius,g=u.xAlign,m=u.yAlign,p=t.x,v=t.y,b=e.width,y=e.height;"center"===m?("left"===g?(i=p,o=i-h,r=i):(i=p+b,o=i+h,r=i),l=v+y/2,s=l-h,d=l+h):("left"===g?(i=p+f,o=i+h,r=o+h):"right"===g?(i=p+b-f,o=i-h,r=o-h):(o=p+b/2,i=o-h,r=o+h),"top"===m?(s=v,l=s-h,d=s):(s=v+y,l=s+h,d=s));var x=a.color(u.backgroundColor);c.fillStyle=x.alpha(n*x.alpha()).rgbString(),c.beginPath(),c.moveTo(i,s),c.lineTo(o,l),c.lineTo(r,d),c.closePath(),c.fill()},drawTitle:function(t,e,n,i){var o=e.title;if(o.length){n.textAlign=e._titleAlign,n.textBaseline="top";var r=e.titleFontSize,s=e.titleSpacing,l=a.color(e.titleFontColor);n.fillStyle=l.alpha(i*l.alpha()).rgbString(),n.font=a.fontString(r,e._titleFontStyle,e._titleFontFamily);var d,u;for(d=0,u=o.length;u>d;++d)n.fillText(o[d],t.x,t.y),t.y+=r+s,d+1===o.length&&(t.y+=e.titleMarginBottom-s)}},drawBody:function(t,e,n,i){var o=e.bodyFontSize,r=e.bodySpacing,s=e.body;n.textAlign=e._bodyAlign,n.textBaseline="top";var l=a.color(e.bodyFontColor),d=l.alpha(i*l.alpha()).rgbString();n.fillStyle=d,n.font=a.fontString(o,e._bodyFontStyle,e._bodyFontFamily);var u=0,c=function(e){n.fillText(e,t.x+u,t.y),t.y+=o+r};a.each(e.beforeBody,c);var h=s.length>1;u=h?o+2:0,a.each(s,function(r,s){a.each(r.before,c),a.each(r.lines,function(r){h&&(n.fillStyle=a.color(e.legendColorBackground).alpha(i).rgbaString(),n.fillRect(t.x,t.y,o,o),n.strokeStyle=a.color(e.labelColors[s].borderColor).alpha(i).rgbaString(),n.strokeRect(t.x,t.y,o,o),n.fillStyle=a.color(e.labelColors[s].backgroundColor).alpha(i).rgbaString(),n.fillRect(t.x+1,t.y+1,o-2,o-2),n.fillStyle=d),c(r)}),a.each(r.after,c)}),u=0,a.each(e.afterBody,c),t.y-=r},drawFooter:function(t,e,n,i){var o=e.footer;if(o.length){t.y+=e.footerMarginTop,n.textAlign=e._footerAlign,n.textBaseline="top";var r=a.color(e.footerFontColor);n.fillStyle=r.alpha(i*r.alpha()).rgbString(),n.font=a.fontString(e.footerFontSize,e._footerFontStyle,e._footerFontFamily),a.each(o,function(i){n.fillText(i,t.x,t.y),t.y+=e.footerFontSize+e.footerSpacing})}},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var n=this.getTooltipSize(e),i={x:e.x,y:e.y},o=Math.abs(e.opacity<.001)?0:e.opacity;if(this._options.enabled){var r=a.color(e.backgroundColor);t.fillStyle=r.alpha(o*r.alpha()).rgbString(),a.drawRoundedRectangle(t,i.x,i.y,n.width,n.height,e.cornerRadius),t.fill(),this.drawCaret(i,n,o),i.x+=e.xPadding,i.y+=e.yPadding,this.drawTitle(i,e,t,o),this.drawBody(i,e,t,o),this.drawFooter(i,e,t,o)}}}})}},{}],35:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=t.defaults.global;n.elements.arc={backgroundColor:n.defaultColor,borderColor:"#fff",borderWidth:2},t.elements.Arc=t.Element.extend({inLabelRange:function(t){var e=this._view;return e?Math.pow(t-e.x,2)l;)l+=2*Math.PI;for(;o>l;)o-=2*Math.PI;for(;s>o;)o+=2*Math.PI;var d=o>=s&&l>=o,u=r>=i.innerRadius&&r<=i.outerRadius;return d&&u}return!1},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(){function t(t,e){var n=e._view;e._view.steppedLine===!0?(l.lineTo(e._view.x,t._view.y),l.lineTo(e._view.x,e._view.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,o=a.spanGaps,r=a.scaleZero,s=i._loop,l=i._chart.ctx;l.save();var d=i._children.slice(),u=-1;s&&d.length&&d.push(d[0]);var c,h,f,g;if(d.length&&a.fill){for(l.beginPath(),c=0;cc;c++)e.lineTo.apply(e,t(c));e.fill(),n.borderWidth&&e.stroke()},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){var n=this._view;return n?n.y=n.x-n.width/2&&t<=n.x+n.width/2&&e>=n.y&&e<=n.base:t>=n.x-n.width/2&&t<=n.x+n.width/2&&e>=n.base&&e<=n.y:!1},inLabelRange:function(t){var e=this._view;return e?t>=e.x-e.width/2&&t<=e.x+e.width/2:!1},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}})}},{}],39:[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=-1!==i?i:t.minIndex),void 0!==t.options.ticks.max&&(i=e.indexOf(n,t.options.ticks.max),t.maxIndex=-1!==i?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){return this.ticks[t]},getPixelForValue:function(t,e,n,i){var a=this,o=Math.max(a.maxIndex+1-a.minIndex-(a.options.gridLines.offsetGridLines?0:1),1);if(void 0!==t&&isNaN(e)){var r=a.getLabels(),s=r.indexOf(t);e=-1!==s?s:e}if(a.isHorizontal()){var l=a.width-(a.paddingLeft+a.paddingRight),d=l/o,u=d*(e-a.minIndex)+a.paddingLeft;return(a.options.gridLines.offsetGridLines&&i||a.maxIndex===a.minIndex&&i)&&(u+=d/2),a.left+Math.round(u)}var c=a.height-(a.paddingTop+a.paddingBottom),h=c/o,f=h*(e-a.minIndex)+a.paddingTop;return a.options.gridLines.offsetGridLines&&i&&(f+=h/2),a.top+Math.round(f)},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(),o=a?n.width-(n.paddingLeft+n.paddingRight):n.height-(n.paddingTop+n.paddingBottom),r=o/i;return t-=a?n.left:n.top,n.options.gridLines.offsetGridLines&&(t-=r/2),t-=a?n.paddingLeft:n.paddingTop,e=0>=t?0:Math.round(t/r)},getBasePixel:function(){return this.bottom}});t.scaleService.registerScaleType("category",i,n)}},{}],40:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n={position:"left",ticks:{callback:function(t,n,i){var a=i.length>3?i[2]-i[1]:i[1]-i[0];Math.abs(a)>1&&t!==Math.floor(t)&&(a=t-Math.floor(t));var o=e.log10(Math.abs(a)),r="";if(0!==t){var s=-1*Math.floor(o);s=Math.max(Math.min(s,20),0),r=t.toFixed(s)}else r="0";return r}}},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,o=a.data,r=o.datasets,s=n.isHorizontal();if(n.min=null,n.max=null,i.stacked){var l={},d=!1,u=!1;e.each(r,function(o,r){var s=a.getDatasetMeta(r);void 0===l[s.type]&&(l[s.type]={positiveValues:[],negativeValues:[]});var c=l[s.type].positiveValues,h=l[s.type].negativeValues;a.isDatasetVisible(r)&&t(s)&&e.each(o.data,function(t,e){var a=+n.getRightValue(t);isNaN(a)||s.data[e].hidden||(c[e]=c[e]||0,h[e]=h[e]||0,i.relativePoints?c[e]=100:0>a?(u=!0,h[e]+=a):(d=!0,c[e]+=a))})}),e.each(l,function(t){var i=t.positiveValues.concat(t.negativeValues),a=e.min(i),o=e.max(i);n.min=null===n.min?a:Math.min(n.min,a),n.max=null===n.max?o:Math.max(n.max,o)})}else e.each(r,function(i,o){var r=a.getDatasetMeta(o);a.isDatasetVisible(o)&&t(r)&&e.each(i.data,function(t,e){var i=+n.getRightValue(t);isNaN(i)||r.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 o=e.getValueOrDefault(a.fontSize,t.defaults.global.defaultFontSize);n=Math.min(a.maxTicksLimit?a.maxTicksLimit:11,Math.ceil(i.height/(2*o)))}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,i=this,a=i.paddingLeft,o=i.paddingBottom,r=i.start,s=+i.getRightValue(t),l=i.end-r;return i.isHorizontal()?(n=i.width-(a+i.paddingRight),e=i.left+n/l*(s-r),Math.round(e+a)):(n=i.height-(i.paddingTop+o),e=i.bottom-o-n/l*(s-r),Math.round(e))},getValueForPixel:function(t){var e=this,n=e.isHorizontal(),i=e.paddingLeft,a=e.paddingBottom,o=n?e.width-(i+e.paddingRight):e.height-(e.paddingTop+a),r=(n?t-e.left-i:e.bottom-a-t)/o;return e.start+(e.end-e.start)*r},getPixelForTick:function(t){return this.getPixelForValue(this.ticksAsNumbers[t])}});t.scaleService.registerScaleType("linear",i,n)}},{}],41:[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),o=e.sign(t.max);0>a&&0>o?t.max=0:a>0&&o>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 t=this,n=t.options,i=t.ticks=[],a=n.ticks,o=e.getValueOrDefault,r=t.getTickLimit();r=Math.max(2,r);var s,l=a.fixedStepSize&&a.fixedStepSize>0||a.stepSize&&a.stepSize>0;if(l)s=o(a.fixedStepSize,a.stepSize);else{var d=e.niceNum(t.max-t.min,!1);s=e.niceNum(d/(r-1),!0)}var u=Math.floor(t.min/s)*s,c=Math.ceil(t.max/s)*s,h=(c-u)/s;h=e.almostEquals(h,Math.round(h),s/1e3)?Math.round(h):Math.ceil(h),i.push(void 0!==a.min?a.min:u);for(var f=1;h>f;++f)i.push(u+f*s);i.push(void 0!==a.max?a.max:c),t.handleDirectionalChanges(),t.max=e.max(i),t.min=e.min(i),a.reverse?(i.reverse(),t.start=t.max,t.end=t.min):(t.start=t.min,t.end=t.max)},convertTicksToLabels:function(){var e=this;e.ticksAsNumbers=e.ticks.slice(),e.zeroLineIndex=e.ticks.indexOf(0),t.Scale.prototype.convertTicksToLabels.call(e)}})}},{}],42:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n={position:"left",ticks:{callback: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():""}}},i=t.Scale.extend({determineDataLimits:function(){function t(t){return d?t.xAxisID===n.id:t.yAxisID===n.id}var n=this,i=n.options,a=i.ticks,o=n.chart,r=o.data,s=r.datasets,l=e.getValueOrDefault,d=n.isHorizontal();if(n.min=null,n.max=null,n.minNotZero=null,i.stacked){var u={};e.each(s,function(a,r){var s=o.getDatasetMeta(r);o.isDatasetVisible(r)&&t(s)&&(void 0===u[s.type]&&(u[s.type]=[]),e.each(a.data,function(t,e){var a=u[s.type],o=+n.getRightValue(t);isNaN(o)||s.data[e].hidden||(a[e]=a[e]||0,i.relativePoints?a[e]=100:a[e]+=o)}))}),e.each(u,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 r=o.getDatasetMeta(a);o.isDatasetVisible(a)&&t(r)&&e.each(i.data,function(t,e){var i=+n.getRightValue(t);isNaN(i)||r.data[e].hidden||(null===n.min?n.min=i:in.max&&(n.max=i),0!==i&&(null===n.minNotZero||it.max&&(t.max=i))})}}),t.handleTickRangeOptions()},getTickLimit:function(){var t=this.options.ticks,i=e.getValueOrDefault(t.fontSize,n.defaultFontSize);return Math.min(t.maxTicksLimit?t.maxTicksLimit:11,Math.ceil(this.drawingArea/(1.5*i)))},convertTicksToLabels:function(){var e=this;t.LinearScaleBase.prototype.convertTicksToLabels.call(e),e.pointLabels=e.chart.data.labels.map(e.options.pointLabels.callback,e)},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var t,i,a,o,r,s,l,d,u,c,h,f,g=this.options.pointLabels,m=e.getValueOrDefault(g.fontSize,n.defaultFontSize),p=e.getValueOrDefault(g.fontStyle,n.defaultFontStyle),v=e.getValueOrDefault(g.fontFamily,n.defaultFontFamily),b=e.fontString(m,p,v),y=e.min([this.height/2-m-5,this.width/2]),x=this.width,k=0;for(this.ctx.font=b,i=0;ix&&(x=t.x+o,r=i),t.x-ow?t.x+a>x&&(x=t.x+a,r=i):t.x-ae&&0>n?n:e>0&&n>0?e:0)},draw:function(){var t=this,i=t.options,a=i.gridLines,o=i.ticks,r=i.angleLines,s=i.pointLabels,l=e.getValueOrDefault;if(i.display){var d=t.ctx,u=l(o.fontSize,n.defaultFontSize),c=l(o.fontStyle,n.defaultFontStyle),h=l(o.fontFamily,n.defaultFontFamily),f=e.fontString(u,c,h);if(e.each(t.ticks,function(r,s){if(s>0||i.reverse){var c=t.getDistanceFromCenterForValue(t.ticksAsNumbers[s]),h=t.yCenter-c;if(a.display&&0!==s)if(d.strokeStyle=e.getValueAtIndexOrDefault(a.color,s-1),d.lineWidth=e.getValueAtIndexOrDefault(a.lineWidth,s-1),i.lineArc)d.beginPath(),d.arc(t.xCenter,t.yCenter,c,0,2*Math.PI),d.closePath(),d.stroke();else{d.beginPath();for(var g=0;g=0;y--){if(r.display){var x=t.getPointPosition(y,g);d.beginPath(),d.moveTo(t.xCenter,t.yCenter),d.lineTo(x.x,x.y),d.stroke(),d.closePath()}var k=t.getPointPosition(y,g+5),S=l(s.fontColor,n.defaultFontColor);d.font=b,d.fillStyle=S;var w=t.pointLabels,_=this.getIndexAngle(y)+Math.PI/2,M=360*_/(2*Math.PI)%360;0===M||180===M?d.textAlign="center":180>M?d.textAlign="left":d.textAlign="right",90===M||270===M?d.textBaseline="middle":M>270||90>M?d.textBaseline="bottom":d.textBaseline="top",d.fillText(w[y]?w[y]:"",k.x,k.y)}}}}});t.scaleService.registerScaleType("radialLinear",a,i)}},{}],44:[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,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}},o=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"undefined"!=typeof this.labelMoments[t]?this.labelMoments[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,o){var r=[],s=t.chart.isDatasetVisible(o);"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),r.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):r=n,t.labelMoments.push(r)},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()},buildTicks:function(){var i=this;i.ctx.save();var a=e.getValueOrDefault(i.options.ticks.fontSize,t.defaults.global.defaultFontSize),o=e.getValueOrDefault(i.options.ticks.fontStyle,t.defaults.global.defaultFontStyle),r=e.getValueOrDefault(i.options.ticks.fontFamily,t.defaults.global.defaultFontFamily),s=e.fontString(a,o,r);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.paddingLeft+i.paddingRight):i.height-(i.paddingTop+i.paddingBottom),d=i.tickFormatFunction(i.firstTick,0,[]),u=i.ctx.measureText(d).width,c=Math.cos(e.toRadians(i.options.ticks.maxRotation)),h=Math.sin(e.toRadians(i.options.ticks.maxRotation));u=u*c+a*h;var f=l/u;i.tickUnit="millisecond",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)k?i.lastTick=i.getMomentStartOf(i.lastTick.add(1,i.tickUnit)):k>=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 S=1;S<=i.scaleSizeInUnits;++S){var w=y.clone().add(S,i.tickUnit);if(i.options.time.max&&w.diff(i.lastTick,i.tickUnit,!0)>=0)break;S%i.unitScale===0&&i.ticks.push(w)}var _=i.ticks[i.ticks.length-1].diff(i.lastTick,i.tickUnit);(0!==_||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()},getLabelForIndex:function(t,e){var n=this,i=n.chart.data.labels&&te||t[3]&&t[3]<1?c(t,e):"rgb("+t[0]+", "+t[1]+", "+t[2]+")"}function c(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"rgba("+t[0]+", "+t[1]+", "+t[2]+", "+e+")"}function h(t,e){if(1>e||t[3]&&t[3]<1)return f(t,e);var n=Math.round(t[0]/255*100),i=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgb("+n+"%, "+i+"%, "+a+"%)"}function f(t,e){var n=Math.round(t[0]/255*100),i=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgba("+n+"%, "+i+"%, "+a+"%, "+(e||t[3]||1)+")"}function g(t,e){return 1>e||t[3]&&t[3]<1?m(t,e):"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"}function m(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"hsla("+t[0]+", "+t[1]+"%, "+t[2]+"%, "+e+")"}function p(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"}function v(t){return k[t.slice(0,3)]}function b(t,e,n){return Math.min(Math.max(e,t),n)}function y(t){var e=t.toString(16).toUpperCase();return e.length<2?"0"+e:e}var x=t(5);e.exports={getRgba:i,getHsla:a,getRgb:r,getHsl:s,getHwb:o,getAlpha:l,hexString:d,rgbString:u,rgbaString:c,percentString:h,percentaString:f,hslString:g,hslaString:m,hwbString:p,keyword:v};var k={};for(var S in x)k[x[S]]=S},{5:5}],2:[function(t,e,n){var i=t(4),a=t(1),o=function(t){if(t instanceof o)return t;if(!(this instanceof o))return new o(t);this.values={rgb:[0,0,0],hsl:[0,0,0],hsv:[0,0,0],hwb:[0,0,0],cmyk:[0,0,0,0],alpha:1};var e;if("string"==typeof t)if(e=a.getRgba(t))this.setValues("rgb",e);else if(e=a.getHsla(t))this.setValues("hsl",e);else{if(!(e=a.getHwb(t)))throw new Error('Unable to parse color from string "'+t+'"');this.setValues("hwb",e)}else if("object"==typeof t)if(e=t,void 0!==e.r||void 0!==e.red)this.setValues("rgb",e);else if(void 0!==e.l||void 0!==e.lightness)this.setValues("hsl",e);else if(void 0!==e.v||void 0!==e.value)this.setValues("hsv",e);else if(void 0!==e.w||void 0!==e.whiteness)this.setValues("hwb",e);else{if(void 0===e.c&&void 0===e.cyan)throw new Error("Unable to parse color from object "+JSON.stringify(t));this.setValues("cmyk",e)}};o.prototype={rgb:function(){return this.setSpace("rgb",arguments)},hsl:function(){return this.setSpace("hsl",arguments)},hsv:function(){return this.setSpace("hsv",arguments)},hwb:function(){return this.setSpace("hwb",arguments)},cmyk:function(){return this.setSpace("cmyk",arguments)},rgbArray:function(){return this.values.rgb},hslArray:function(){return this.values.hsl},hsvArray:function(){return this.values.hsv},hwbArray:function(){var t=this.values;return 1!==t.alpha?t.hwb.concat([t.alpha]):t.hwb},cmykArray:function(){return this.values.cmyk},rgbaArray:function(){var t=this.values;return t.rgb.concat([t.alpha])},hslaArray:function(){var t=this.values;return t.hsl.concat([t.alpha])},alpha:function(t){return void 0===t?this.values.alpha:(this.setValues("alpha",t),this)},red:function(t){return this.setChannel("rgb",0,t)},green:function(t){return this.setChannel("rgb",1,t)},blue:function(t){return this.setChannel("rgb",2,t)},hue:function(t){return t&&(t%=360,t=0>t?360+t:t),this.setChannel("hsl",0,t)},saturation:function(t){return this.setChannel("hsl",1,t)},lightness:function(t){return this.setChannel("hsl",2,t)},saturationv:function(t){return this.setChannel("hsv",1,t)},whiteness:function(t){return this.setChannel("hwb",1,t)},blackness:function(t){return this.setChannel("hwb",2,t)},value:function(t){return this.setChannel("hsv",2,t)},cyan:function(t){return this.setChannel("cmyk",0,t)},magenta:function(t){return this.setChannel("cmyk",1,t)},yellow:function(t){return this.setChannel("cmyk",2,t)},black:function(t){return this.setChannel("cmyk",3,t)},hexString:function(){return a.hexString(this.values.rgb)},rgbString:function(){return a.rgbString(this.values.rgb,this.values.alpha)},rgbaString:function(){return a.rgbaString(this.values.rgb,this.values.alpha)},percentString:function(){return a.percentString(this.values.rgb,this.values.alpha)},hslString:function(){return a.hslString(this.values.hsl,this.values.alpha)},hslaString:function(){return a.hslaString(this.values.hsl,this.values.alpha)},hwbString:function(){return a.hwbString(this.values.hwb,this.values.alpha)},keyword:function(){return a.keyword(this.values.rgb,this.values.alpha)},rgbNumber:function(){var t=this.values.rgb;return t[0]<<16|t[1]<<8|t[2]},luminosity:function(){for(var t=this.values.rgb,e=[],n=0;n=i?i/12.92:Math.pow((i+.055)/1.055,2.4)}return.2126*e[0]+.7152*e[1]+.0722*e[2]},contrast:function(t){var e=this.luminosity(),n=t.luminosity();return e>n?(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 128>e},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;3>e;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]=0>n?360+n:n,this.setValues("hsl",e),this},mix:function(t,e){var n=this,i=t,a=void 0===e?.5:e,o=2*a-1,r=n.alpha()-i.alpha(),s=((o*r===-1?o:(o+r)/(1+o*r))+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 o,i=this.values,a=n.values;for(var r in i)i.hasOwnProperty(r)&&(t=i[r],e={}.toString.call(t),"[object Array]"===e?a[r]=t.slice(0):"[object Number]"===e?a[r]=t:console.error("unexpected color value:",t));return n}},o.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},o.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},o.prototype.getValues=function(t){for(var e=this.values,n={},i=0;ie&&(e+=360),i=(s+l)/2,n=l==s?0:.5>=i?d/(l+s):d/(2-l-s),[e,100*n,100*i]}function a(t){var e,n,i,a=t[0],o=t[1],r=t[2],s=Math.min(a,o,r),l=Math.max(a,o,r),d=l-s;return n=0==l?0:d/l*1e3/10,l==s?e=0:a==l?e=(o-r)/d:o==l?e=2+(r-a)/d:r==l&&(e=4+(a-o)/d),e=Math.min(60*e,360),0>e&&(e+=360),i=l/255*1e3/10,[e,n,i]}function o(t){var e=t[0],n=t[1],a=t[2],o=i(t)[0],r=1/255*Math.min(e,Math.min(n,a)),a=1-1/255*Math.max(e,Math.max(n,a));return[o,100*r,100*a]}function s(t){var e,n,i,a,o=t[0]/255,r=t[1]/255,s=t[2]/255;return a=Math.min(1-o,1-r,1-s),e=(1-o-a)/(1-a)||0,n=(1-r-a)/(1-a)||0,i=(1-s-a)/(1-a)||0,[100*e,100*n,100*i,100*a]}function l(t){return K[JSON.stringify(t)]}function d(t){var e=t[0]/255,n=t[1]/255,i=t[2]/255;e=e>.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,o=.2126*e+.7152*n+.0722*i,r=.0193*e+.1192*n+.9505*i;return[100*a,100*o,100*r]}function u(t){var e,n,i,a=d(t),o=a[0],r=a[1],s=a[2];return o/=95.047,r/=100,s/=108.883,o=o>.008856?Math.pow(o,1/3):7.787*o+16/116,r=r>.008856?Math.pow(r,1/3):7.787*r+16/116,s=s>.008856?Math.pow(s,1/3):7.787*s+16/116,e=116*r-16,n=500*(o-r),i=200*(r-s),[e,n,i]}function c(t){return B(u(t))}function h(t){var e,n,i,a,o,r=t[0]/360,s=t[1]/100,l=t[2]/100;if(0==s)return o=255*l,[o,o,o];n=.5>l?l*(1+s):l+s-l*s,e=2*l-n,a=[0,0,0];for(var d=0;3>d;d++)i=r+1/3*-(d-1),0>i&&i++,i>1&&i--,o=1>6*i?e+6*(n-e)*i:1>2*i?n:2>3*i?e+(n-e)*(2/3-i)*6:e,a[d]=255*o;return a}function f(t){var e,n,i=t[0],a=t[1]/100,o=t[2]/100;return 0===o?[0,0,0]:(o*=2,a*=1>=o?o:2-o,n=(o+a)/2,e=2*a/(o+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,o=e-Math.floor(e),r=255*i*(1-n),s=255*i*(1-n*o),l=255*i*(1-n*(1-o)),i=255*i;switch(a){case 0:return[i,l,r];case 1:return[s,i,r];case 2:return[r,i,l];case 3:return[r,s,i];case 4:return[l,r,i];case 5:return[i,r,s]}}function x(t){var e,n,i=t[0],a=t[1]/100,o=t[2]/100;return n=(2-a)*o,e=a*o,e/=1>=n?n:2-n,e=e||0,n/=2,[i,100*e,100*n]}function k(t){return o(y(t))}function S(t){return s(y(t))}function w(t){return l(y(t))}function _(t){var e,n,i,a,o=t[0]/360,s=t[1]/100,l=t[2]/100,d=s+l;switch(d>1&&(s/=d,l/=d),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(_(t))}function D(t){return a(_(t))}function C(t){return s(_(t))}function T(t){return l(_(t))}function P(t){var e,n,i,a=t[0]/100,o=t[1]/100,r=t[2]/100,s=t[3]/100;return e=1-Math.min(1,a*(1-s)+s),n=1-Math.min(1,o*(1-s)+s),i=1-Math.min(1,r*(1-s)+s),[255*e,255*n,255*i]}function F(t){return i(P(t))}function I(t){return a(P(t))}function A(t){return o(P(t))}function O(t){return l(P(t))}function R(t){var e,n,i,a=t[0]/100,o=t[1]/100,r=t[2]/100;return e=3.2406*a+-1.5372*o+r*-.4986,n=a*-.9689+1.8758*o+.0415*r,i=.0557*a+o*-.204+1.057*r,e=e>.0031308?1.055*Math.pow(e,1/2.4)-.055:e=12.92*e,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:n=12.92*n,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:i=12.92*i,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 W(t){var e,n,i,a=t[0],o=t[1],r=t[2];return a/=95.047,o/=100,r/=108.883,a=a>.008856?Math.pow(a,1/3):7.787*a+16/116,o=o>.008856?Math.pow(o,1/3):7.787*o+16/116,r=r>.008856?Math.pow(r,1/3):7.787*r+16/116,e=116*o-16,n=500*(a-o),i=200*(o-r),[e,n,i]}function L(t){return B(W(t))}function V(t){var e,n,i,a,o=t[0],r=t[1],s=t[2];return 8>=o?(n=100*o/903.3,a=7.787*(n/100)+16/116):(n=100*Math.pow((o+16)/116,3),a=Math.pow(n/100,1/3)),e=.008856>=e/95.047?e=95.047*(r/500+a-16/116)/7.787:95.047*Math.pow(r/500+a,3),i=.008859>=i/108.883?i=108.883*(a-s/200-16/116)/7.787:108.883*Math.pow(a-s/200,3),[e,n,i]}function B(t){var e,n,i,a=t[0],o=t[1],r=t[2];return e=Math.atan2(r,o),n=360*e/2/Math.PI,0>n&&(n+=360),i=Math.sqrt(o*o+r*r),[a,i,n]}function Y(t){return R(V(t))}function z(t){var e,n,i,a=t[0],o=t[1],r=t[2];return i=r/360*2*Math.PI,e=o*Math.cos(i),n=o*Math.sin(i),[a,e,n]}function H(t){return V(z(t))}function N(t){return Y(z(t))}function E(t){return X[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 u(E(t))}function J(t){return d(E(t))}e.exports={rgb2hsl:i,rgb2hsv:a,rgb2hwb:o,rgb2cmyk:s,rgb2keyword:l,rgb2xyz:d,rgb2lab:u,rgb2lch:c,hsl2rgb:h,hsl2hsv:f,hsl2hwb:m,hsl2cmyk:p,hsl2keyword:v,hsv2rgb:y,hsv2hsl:x,hsv2hwb:k,hsv2cmyk:S,hsv2keyword:w,hwb2rgb:_,hwb2hsl:M,hwb2hsv:D,hwb2cmyk:C,hwb2keyword:T,cmyk2rgb:P,cmyk2hsl:F,cmyk2hsv:I,cmyk2hwb:A,cmyk2keyword:O,keyword2rgb:E,keyword2hsl:U,keyword2hsv:j,keyword2hwb:G,keyword2cmyk:q,keyword2lab:Z,keyword2xyz:J,xyz2rgb:R,xyz2lab:W,xyz2lch:L,lab2xyz:V,lab2rgb:Y,lab2lch:B,lch2lab:z,lch2xyz:H,lch2rgb:N};var X={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 X)K[JSON.stringify(X[Q])]=Q},{}],4:[function(t,e,n){var i=t(3),a=function(){return new d};for(var o in i){a[o+"Raw"]=function(t){return function(e){return"number"==typeof e&&(e=Array.prototype.slice.call(arguments)),i[t](e)}}(o);var r=/(\w+)2(\w+)/.exec(o),s=r[1],l=r[2];a[s]=a[s]||{},a[s][l]=a[o]=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 vi)i=vi[n],a=e[i],p(a)||(t[i]=a);return t}function b(e){v(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),bi===!1&&(bi=!0,t.updateOffset(this),bi=!1)}function y(t){return t instanceof b||null!=t&&null!=t._isAMomentObject}function x(t){return 0>t?Math.ceil(t)||0:Math.floor(t)}function k(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=x(e)),n}function S(t,e,n){var i,a=Math.min(t.length,e.length),o=Math.abs(t.length-e.length),r=0;for(i=0;a>i;i++)(n&&t[i]!==e[i]||!n&&k(t[i])!==k(e[i]))&&r++;return r+o}function w(e){t.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+e)}function _(e,n){var i=!0;return u(function(){if(null!=t.deprecationHandler&&t.deprecationHandler(null,e),i){for(var a,o=[],r=0;r0?"future":"past"];return D(n)?n(e):n.replace(/%s/i,e)}function L(t,e){var n=t.toLowerCase();Ti[n]=Ti[n+"s"]=Ti[e]=t}function V(t){return"string"==typeof t?Ti[t]||Ti[t.toLowerCase()]:void 0}function B(t){var e,n,i={};for(n in t)d(t,n)&&(e=V(n),e&&(i[e]=t[n]));return i}function Y(t,e){Pi[t]=e}function z(t){var e=[];for(var n in t)e.push({unit:n,priority:Pi[n]});return e.sort(function(t,e){return t.priority-e.priority}),e}function H(e,n){return function(i){return null!=i?(E(this,e,i),t.updateOffset(this,n),this):N(this,e)}}function N(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function E(t,e,n){t.isValid()&&t._d["set"+(t._isUTC?"UTC":"")+e](n)}function U(t){return t=V(t),D(this[t])?this[t]():this}function j(t,e){if("object"==typeof t){t=B(t);for(var n=z(t),i=0;i=0;return(o?n?"+":"":"-")+Math.pow(10,Math.max(0,a)).toString().substr(1)+i}function q(t,e,n,i){var a=i;"string"==typeof i&&(a=function(){return this[i]()}),t&&(Oi[t]=a),e&&(Oi[e[0]]=function(){return G(a.apply(this,arguments),e[1],e[2])}),n&&(Oi[n]=function(){return this.localeData().ordinal(a.apply(this,arguments),t)})}function Z(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function J(t){var e,n,i=t.match(Fi);for(e=0,n=i.length;n>e;e++)Oi[i[e]]?i[e]=Oi[i[e]]:i[e]=Z(i[e]);return function(e){var a,o="";for(a=0;n>a;a++)o+=i[a]instanceof Function?i[a].call(e,t):i[a];return o}}function X(t,e){return t.isValid()?(e=K(e,t.localeData()),Ai[e]=Ai[e]||J(e),Ai[e](t)):t.localeData().invalidDate()}function K(t,e){function n(t){return e.longDateFormat(t)||t}var i=5;for(Ii.lastIndex=0;i>=0&&Ii.test(t);)t=t.replace(Ii,n),Ii.lastIndex=0,i-=1;return t}function Q(t,e,n){Ki[t]=D(e)?e:function(t,i){return t&&n?n:e}}function $(t,e){return d(Ki,t)?Ki[t](e._strict,e._locale):new RegExp(tt(t))}function tt(t){return et(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,n,i,a){return e||n||i||a}))}function et(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function nt(t,e){var n,i=e;for("string"==typeof t&&(t=[t]),"number"==typeof e&&(i=function(t,n){n[e]=k(t)}),n=0;ni;++i)o=c([2e3,i]),this._shortMonthsParse[i]=this.monthsShort(o,"").toLocaleLowerCase(),this._longMonthsParse[i]=this.months(o,"").toLocaleLowerCase();return n?"MMM"===e?(a=ki.call(this._shortMonthsParse,r),-1!==a?a:null):(a=ki.call(this._longMonthsParse,r),-1!==a?a:null):"MMM"===e?(a=ki.call(this._shortMonthsParse,r),-1!==a?a:(a=ki.call(this._longMonthsParse,r),-1!==a?a:null)):(a=ki.call(this._longMonthsParse,r),-1!==a?a:(a=ki.call(this._shortMonthsParse,r),-1!==a?a:null))}function dt(t,e,n){var i,a,o;if(this._monthsParseExact)return lt.call(this,t,e,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),i=0;12>i;i++){if(a=c([2e3,i]),n&&!this._longMonthsParse[i]&&(this._longMonthsParse[i]=new RegExp("^"+this.months(a,"").replace(".","")+"$","i"),this._shortMonthsParse[i]=new RegExp("^"+this.monthsShort(a,"").replace(".","")+"$","i")),n||this._monthsParse[i]||(o="^"+this.months(a,"")+"|^"+this.monthsShort(a,""),this._monthsParse[i]=new RegExp(o.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[i].test(t))return i;if(n&&"MMM"===e&&this._shortMonthsParse[i].test(t))return i;if(!n&&this._monthsParse[i].test(t))return i}}function ut(t,e){var n;if(!t.isValid())return t;if("string"==typeof e)if(/^\d+$/.test(e))e=k(e);else if(e=t.localeData().monthsParse(e),"number"!=typeof e)return t;return n=Math.min(t.date(),ot(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,n),t}function ct(e){return null!=e?(ut(this,e), +t.updateOffset(this,!0),this):N(this,"Month")}function ht(){return ot(this.year(),this.month())}function ft(t){return this._monthsParseExact?(d(this,"_monthsRegex")||mt.call(this),t?this._monthsShortStrictRegex:this._monthsShortRegex):(d(this,"_monthsShortRegex")||(this._monthsShortRegex=ca),this._monthsShortStrictRegex&&t?this._monthsShortStrictRegex:this._monthsShortRegex)}function gt(t){return this._monthsParseExact?(d(this,"_monthsRegex")||mt.call(this),t?this._monthsStrictRegex:this._monthsRegex):(d(this,"_monthsRegex")||(this._monthsRegex=ha),this._monthsStrictRegex&&t?this._monthsStrictRegex:this._monthsRegex)}function mt(){function t(t,e){return e.length-t.length}var e,n,i=[],a=[],o=[];for(e=0;12>e;e++)n=c([2e3,e]),i.push(this.monthsShort(n,"")),a.push(this.months(n,"")),o.push(this.months(n,"")),o.push(this.monthsShort(n,""));for(i.sort(t),a.sort(t),o.sort(t),e=0;12>e;e++)i[e]=et(i[e]),a[e]=et(a[e]);for(e=0;24>e;e++)o[e]=et(o[e]);this._monthsRegex=new RegExp("^("+o.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+i.join("|")+")","i")}function pt(t){return vt(t)?366:365}function vt(t){return t%4===0&&t%100!==0||t%400===0}function bt(){return vt(this.year())}function yt(t,e,n,i,a,o,r){var s=new Date(t,e,n,i,a,o,r);return 100>t&&t>=0&&isFinite(s.getFullYear())&&s.setFullYear(t),s}function xt(t){var e=new Date(Date.UTC.apply(null,arguments));return 100>t&&t>=0&&isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t),e}function kt(t,e,n){var i=7+e-n,a=(7+xt(t,0,i).getUTCDay()-e)%7;return-a+i-1}function St(t,e,n,i,a){var o,r,s=(7+n-i)%7,l=kt(t,i,a),d=1+7*(e-1)+s+l;return 0>=d?(o=t-1,r=pt(o)+d):d>pt(t)?(o=t+1,r=d-pt(t)):(o=t,r=d),{year:o,dayOfYear:r}}function wt(t,e,n){var i,a,o=kt(t.year(),e,n),r=Math.floor((t.dayOfYear()-o-1)/7)+1;return 1>r?(a=t.year()-1,i=r+_t(a,e,n)):r>_t(t.year(),e,n)?(i=r-_t(t.year(),e,n),a=t.year()+1):(a=t.year(),i=r),{week:i,year:a}}function _t(t,e,n){var i=kt(t,e,n),a=kt(t+1,e,n);return(pt(t)-i+a)/7}function Mt(t){return wt(t,this._week.dow,this._week.doy).week}function Dt(){return this._week.dow}function Ct(){return this._week.doy}function Tt(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function Pt(t){var e=wt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")}function Ft(t,e){return"string"!=typeof t?t:isNaN(t)?(t=e.weekdaysParse(t),"number"==typeof t?t:null):parseInt(t,10)}function It(t,e){return"string"==typeof t?e.weekdaysParse(t)%7||7:isNaN(t)?null:t}function At(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 Ot(t){return t?this._weekdaysShort[t.day()]:this._weekdaysShort}function Rt(t){return t?this._weekdaysMin[t.day()]:this._weekdaysMin}function Wt(t,e,n){var i,a,o,r=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],i=0;7>i;++i)o=c([2e3,1]).day(i),this._minWeekdaysParse[i]=this.weekdaysMin(o,"").toLocaleLowerCase(),this._shortWeekdaysParse[i]=this.weekdaysShort(o,"").toLocaleLowerCase(),this._weekdaysParse[i]=this.weekdays(o,"").toLocaleLowerCase();return n?"dddd"===e?(a=ki.call(this._weekdaysParse,r),-1!==a?a:null):"ddd"===e?(a=ki.call(this._shortWeekdaysParse,r),-1!==a?a:null):(a=ki.call(this._minWeekdaysParse,r),-1!==a?a:null):"dddd"===e?(a=ki.call(this._weekdaysParse,r),-1!==a?a:(a=ki.call(this._shortWeekdaysParse,r),-1!==a?a:(a=ki.call(this._minWeekdaysParse,r),-1!==a?a:null))):"ddd"===e?(a=ki.call(this._shortWeekdaysParse,r),-1!==a?a:(a=ki.call(this._weekdaysParse,r),-1!==a?a:(a=ki.call(this._minWeekdaysParse,r),-1!==a?a:null))):(a=ki.call(this._minWeekdaysParse,r),-1!==a?a:(a=ki.call(this._weekdaysParse,r),-1!==a?a:(a=ki.call(this._shortWeekdaysParse,r),-1!==a?a:null)))}function Lt(t,e,n){var i,a,o;if(this._weekdaysParseExact)return Wt.call(this,t,e,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),i=0;7>i;i++){if(a=c([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]||(o="^"+this.weekdays(a,"")+"|^"+this.weekdaysShort(a,"")+"|^"+this.weekdaysMin(a,""),this._weekdaysParse[i]=new RegExp(o.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 Vt(t){if(!this.isValid())return null!=t?this:NaN;var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=Ft(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 Yt(t){if(!this.isValid())return null!=t?this:NaN;if(null!=t){var e=It(t,this.localeData());return this.day(this.day()%7?e:e-7)}return this.day()||7}function zt(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Et.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(d(this,"_weekdaysRegex")||(this._weekdaysRegex=ba),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)}function Ht(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Et.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(d(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ya),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Nt(t){return this._weekdaysParseExact?(d(this,"_weekdaysRegex")||Et.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(d(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=xa),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Et(){function t(t,e){return e.length-t.length}var e,n,i,a,o,r=[],s=[],l=[],d=[];for(e=0;7>e;e++)n=c([2e3,1]).day(e),i=this.weekdaysMin(n,""),a=this.weekdaysShort(n,""),o=this.weekdays(n,""),r.push(i),s.push(a),l.push(o),d.push(i),d.push(a),d.push(o);for(r.sort(t),s.sort(t),l.sort(t),d.sort(t),e=0;7>e;e++)s[e]=et(s[e]),l[e]=et(l[e]),d[e]=et(d[e]);this._weekdaysRegex=new RegExp("^("+d.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("^("+r.join("|")+")","i")}function Ut(){return this.hours()%12||12}function jt(){return this.hours()||24}function Gt(t,e){q(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function qt(t,e){return e._meridiemParse}function Zt(t){return"p"===(t+"").toLowerCase().charAt(0)}function Jt(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"}function Xt(t){return t?t.toLowerCase().replace("_","-"):t}function Kt(t){for(var e,n,i,a,o=0;o0;){if(i=Qt(a.slice(0,e).join("-")))return i;if(n&&n.length>=e&&S(a,n,!0)>=e-1)break;e--}o++}return null}function Qt(t){var i=null;if(!Ma[t]&&"undefined"!=typeof n&&n&&n.exports)try{i=ka._abbr,e("./locale/"+t),$t(i)}catch(a){}return Ma[t]}function $t(t,e){var n;return t&&(n=p(e)?ne(t):te(t,e),n&&(ka=n)),ka._abbr}function te(t,e){if(null!==e){var n=_a;return e.abbr=t,null!=Ma[t]?(M("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=Ma[t]._config):null!=e.parentLocale&&(null!=Ma[e.parentLocale]?n=Ma[e.parentLocale]._config:M("parentLocaleUndefined","specified parentLocale is not defined yet. See http://momentjs.com/guides/#/warnings/parent-locale/")),Ma[t]=new P(T(n,e)),$t(t),Ma[t]}return delete Ma[t],null}function ee(t,e){if(null!=e){var n,i=_a;null!=Ma[t]&&(i=Ma[t]._config),e=T(i,e),n=new P(e),n.parentLocale=Ma[t],Ma[t]=n,$t(t)}else null!=Ma[t]&&(null!=Ma[t].parentLocale?Ma[t]=Ma[t].parentLocale:null!=Ma[t]&&delete Ma[t]);return Ma[t]}function ne(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return ka;if(!a(t)){if(e=Qt(t))return e;t=[t]}return Kt(t)}function ie(){return xi(Ma)}function ae(t){var e,n=t._a;return n&&-2===f(t).overflow&&(e=n[ta]<0||n[ta]>11?ta:n[ea]<1||n[ea]>ot(n[$i],n[ta])?ea:n[na]<0||n[na]>24||24===n[na]&&(0!==n[ia]||0!==n[aa]||0!==n[oa])?na:n[ia]<0||n[ia]>59?ia:n[aa]<0||n[aa]>59?aa:n[oa]<0||n[oa]>999?oa:-1,f(t)._overflowDayOfYear&&($i>e||e>ea)&&(e=ea),f(t)._overflowWeeks&&-1===e&&(e=ra),f(t)._overflowWeekday&&-1===e&&(e=sa),f(t).overflow=e),t}function oe(t){var e,n,i,a,o,r,s=t._i,l=Da.exec(s)||Ca.exec(s);if(l){for(f(t).iso=!0,e=0,n=Pa.length;n>e;e++)if(Pa[e][1].exec(l[1])){a=Pa[e][0],i=Pa[e][2]!==!1;break}if(null==a)return void(t._isValid=!1);if(l[3]){for(e=0,n=Fa.length;n>e;e++)if(Fa[e][1].exec(l[3])){o=(l[2]||" ")+Fa[e][0];break}if(null==o)return void(t._isValid=!1)}if(!i&&null!=o)return void(t._isValid=!1);if(l[4]){if(!Ta.exec(l[4]))return void(t._isValid=!1);r="Z"}t._f=a+(o||"")+(r||""),ce(t)}else t._isValid=!1}function re(e){var n=Ia.exec(e._i);return null!==n?void(e._d=new Date(+n[1])):(oe(e),void(e._isValid===!1&&(delete e._isValid,t.createFromInputFallback(e))))}function se(t,e,n){return null!=t?t:null!=e?e:n}function le(e){var n=new Date(t.now());return e._useUTC?[n.getUTCFullYear(),n.getUTCMonth(),n.getUTCDate()]:[n.getFullYear(),n.getMonth(),n.getDate()]}function de(t){var e,n,i,a,o=[];if(!t._d){for(i=le(t),t._w&&null==t._a[ea]&&null==t._a[ta]&&ue(t),t._dayOfYear&&(a=se(t._a[$i],i[$i]),t._dayOfYear>pt(a)&&(f(t)._overflowDayOfYear=!0),n=xt(a,0,t._dayOfYear),t._a[ta]=n.getUTCMonth(),t._a[ea]=n.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=o[e]=i[e];for(;7>e;e++)t._a[e]=o[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[na]&&0===t._a[ia]&&0===t._a[aa]&&0===t._a[oa]&&(t._nextDay=!0,t._a[na]=0),t._d=(t._useUTC?xt:yt).apply(null,o),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[na]=24)}}function ue(t){var e,n,i,a,o,r,s,l;e=t._w,null!=e.GG||null!=e.W||null!=e.E?(o=1,r=4,n=se(e.GG,t._a[$i],wt(ye(),1,4).year),i=se(e.W,1),a=se(e.E,1),(1>a||a>7)&&(l=!0)):(o=t._locale._week.dow,r=t._locale._week.doy,n=se(e.gg,t._a[$i],wt(ye(),o,r).year),i=se(e.w,1),null!=e.d?(a=e.d,(0>a||a>6)&&(l=!0)):null!=e.e?(a=e.e+o,(e.e<0||e.e>6)&&(l=!0)):a=o),1>i||i>_t(n,o,r)?f(t)._overflowWeeks=!0:null!=l?f(t)._overflowWeekday=!0:(s=St(n,i,a,o,r),t._a[$i]=s.year,t._dayOfYear=s.dayOfYear)}function ce(e){if(e._f===t.ISO_8601)return void oe(e);e._a=[],f(e).empty=!0;var n,i,a,o,r,s=""+e._i,l=s.length,d=0;for(a=K(e._f,e._locale).match(Fi)||[],n=0;n0&&f(e).unusedInput.push(r),s=s.slice(s.indexOf(i)+i.length),d+=i.length),Oi[o]?(i?f(e).empty=!1:f(e).unusedTokens.push(o),at(o,i,e)):e._strict&&!i&&f(e).unusedTokens.push(o);f(e).charsLeftOver=l-d,s.length>0&&f(e).unusedInput.push(s),e._a[na]<=12&&f(e).bigHour===!0&&e._a[na]>0&&(f(e).bigHour=void 0),f(e).parsedDateParts=e._a.slice(0),f(e).meridiem=e._meridiem,e._a[na]=he(e._locale,e._a[na],e._meridiem),de(e),ae(e)}function he(t,e,n){var i;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?(i=t.isPM(n),i&&12>e&&(e+=12),i||12!==e||(e=0),e):e}function fe(t){var e,n,i,a,o;if(0===t._f.length)return f(t).invalidFormat=!0,void(t._d=new Date(NaN));for(a=0;ao)&&(i=o,n=e));u(t,n||e)}function ge(t){if(!t._d){var e=B(t._i);t._a=l([e.year,e.month,e.day||e.date,e.hour,e.minute,e.second,e.millisecond],function(t){return t&&parseInt(t,10)}),de(t)}}function me(t){var e=new b(ae(pe(t)));return e._nextDay&&(e.add(1,"d"),e._nextDay=void 0),e}function pe(t){var e=t._i,n=t._f;return t._locale=t._locale||ne(t._l),null===e||void 0===n&&""===e?m({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),y(e)?new b(ae(e)):(a(n)?fe(t):s(e)?t._d=e:n?ce(t):ve(t),g(t)||(t._d=null),t))}function ve(e){var n=e._i;void 0===n?e._d=new Date(t.now()):s(n)?e._d=new Date(n.valueOf()):"string"==typeof n?re(e):a(n)?(e._a=l(n.slice(0),function(t){return parseInt(t,10)}),de(e)):"object"==typeof n?ge(e):"number"==typeof n?e._d=new Date(n):t.createFromInputFallback(e)}function be(t,e,n,i,s){var l={};return"boolean"==typeof n&&(i=n,n=void 0),(o(t)&&r(t)||a(t)&&0===t.length)&&(t=void 0),l._isAMomentObject=!0,l._useUTC=l._isUTC=s,l._l=n,l._i=t,l._f=e,l._strict=i,me(l)}function ye(t,e,n,i){return be(t,e,n,i,!1)}function xe(t,e){var n,i;if(1===e.length&&a(e[0])&&(e=e[0]),!e.length)return ye();for(n=e[0],i=1;it?-1*Math.round(-1*t):Math.round(t)}function De(t,e){q(t,0,0,function(){var t=this.utcOffset(),n="+";return 0>t&&(t=-t,n="-"),n+G(~~(t/60),2)+e+G(~~t%60,2)})}function Ce(t,e){var n=(e||"").match(t)||[],i=n[n.length-1]||[],a=(i+"").match(Wa)||["-",0,0],o=+(60*a[1])+k(a[2]);return"+"===a[0]?o:-o}function Te(e,n){var i,a;return n._isUTC?(i=n.clone(),a=(y(e)||s(e)?e.valueOf():ye(e).valueOf())-i.valueOf(),i._d.setTime(i._d.valueOf()+a),t.updateOffset(i,!1),i):ye(e).local()}function Pe(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function Fe(e,n){var i,a=this._offset||0;return this.isValid()?null!=e?("string"==typeof e?e=Ce(Zi,e):Math.abs(e)<16&&(e=60*e),!this._isUTC&&n&&(i=Pe(this)),this._offset=e,this._isUTC=!0,null!=i&&this.add(i,"m"),a!==e&&(!n||this._changeInProgress?Ge(this,He(e-a,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,t.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?a:Pe(this):null!=e?this:NaN}function Ie(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()}function Ae(t){return this.utcOffset(0,t)}function Oe(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Pe(this),"m")),this}function Re(){if(this._tzm)this.utcOffset(this._tzm);else if("string"==typeof this._i){var t=Ce(qi,this._i);0===t?this.utcOffset(0,!0):this.utcOffset(Ce(qi,this._i))}return this}function We(t){return this.isValid()?(t=t?ye(t).utcOffset():0,(this.utcOffset()-t)%60===0):!1}function Le(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Ve(){if(!p(this._isDSTShifted))return this._isDSTShifted;var t={};if(v(t,this),t=pe(t),t._a){var e=t._isUTC?c(t._a):ye(t._a);this._isDSTShifted=this.isValid()&&S(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Be(){return this.isValid()?!this._isUTC:!1}function Ye(){return this.isValid()?this._isUTC:!1}function ze(){return this.isValid()?this._isUTC&&0===this._offset:!1}function He(t,e){var n,i,a,o=t,r=null;return _e(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(r=La.exec(t))?(n="-"===r[1]?-1:1,o={y:0,d:k(r[ea])*n,h:k(r[na])*n,m:k(r[ia])*n,s:k(r[aa])*n,ms:k(Me(1e3*r[oa]))*n}):(r=Va.exec(t))?(n="-"===r[1]?-1:1,o={y:Ne(r[2],n),M:Ne(r[3],n),w:Ne(r[4],n),d:Ne(r[5],n),h:Ne(r[6],n),m:Ne(r[7],n),s:Ne(r[8],n)}):null==o?o={}:"object"==typeof o&&("from"in o||"to"in o)&&(a=Ue(ye(o.from),ye(o.to)),o={},o.ms=a.milliseconds,o.M=a.months),i=new we(o),_e(t)&&d(t,"_locale")&&(i._locale=t._locale),i}function Ne(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Ee(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 Ue(t,e){var n;return t.isValid()&&e.isValid()?(e=Te(e,t),t.isBefore(e)?n=Ee(t,e):(n=Ee(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,o;return null===i||isNaN(+i)||(M(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."),o=n,n=i,i=o),n="string"==typeof n?+n:n,a=He(n,i),Ge(this,a,t),this}}function Ge(e,n,i,a){var o=n._milliseconds,r=Me(n._days),s=Me(n._months);e.isValid()&&(a=null==a?!0:a,o&&e._d.setTime(e._d.valueOf()+o*i),r&&E(e,"Date",N(e,"Date")+r*i),s&&ut(e,N(e,"Month")+s*i),a&&t.updateOffset(e,r||s))}function qe(t,e){var n=t.diff(e,"days",!0);return-6>n?"sameElse":-1>n?"lastWeek":0>n?"lastDay":1>n?"sameDay":2>n?"nextDay":7>n?"nextWeek":"sameElse"}function Ze(e,n){var i=e||ye(),a=Te(i,this).startOf("day"),o=t.calendarFormat(this,a)||"sameElse",r=n&&(D(n[o])?n[o].call(this,i):n[o]);return this.format(r||this.localeData().calendar(o,this,ye(i)))}function Je(){return new b(this)}function Xe(t,e){var n=y(t)?t:ye(t);return this.isValid()&&n.isValid()?(e=V(p(e)?"millisecond":e),"millisecond"===e?this.valueOf()>n.valueOf():n.valueOf()e-o?(n=t.clone().add(a-1,"months"),i=(e-o)/(o-n)):(n=t.clone().add(a+1,"months"),i=(e-o)/(n-o)),-(a+i)||0}function on(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function rn(){var t=this.clone().utc();return 0o&&(e=o),An.call(this,t,e,n,i,a))}function An(t,e,n,i,a){var o=St(t,e,n,i,a),r=xt(o.year,0,o.dayOfYear);return this.year(r.getUTCFullYear()),this.month(r.getUTCMonth()),this.date(r.getUTCDate()),this}function On(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)}function Rn(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[oa]=k(1e3*("0."+t))}function Ln(){return this._isUTC?"UTC":""}function Vn(){return this._isUTC?"Coordinated Universal Time":""}function Bn(t){return ye(1e3*t)}function Yn(){return ye.apply(null,arguments).parseZone()}function zn(t){return t}function Hn(t,e,n,i){var a=ne(),o=c().set(i,e);return a[n](o,t)}function Nn(t,e,n){if("number"==typeof t&&(e=t,t=void 0),t=t||"",null!=e)return Hn(t,e,n,"month");var i,a=[];for(i=0;12>i;i++)a[i]=Hn(t,i,n,"month");return a}function En(t,e,n,i){"boolean"==typeof t?("number"==typeof e&&(n=e,e=void 0),e=e||""):(e=t,n=e,t=!1,"number"==typeof e&&(n=e,e=void 0),e=e||"");var a=ne(),o=t?a._week.dow:0;if(null!=n)return Hn(e,(n+o)%7,i,"day");var r,s=[];for(r=0;7>r;r++)s[r]=Hn(e,(r+o)%7,i,"day");return s}function Un(t,e){return Nn(t,e,"months")}function jn(t,e){return Nn(t,e,"monthsShort")}function Gn(t,e,n){return En(t,e,n,"weekdays")}function qn(t,e,n){return En(t,e,n,"weekdaysShort")}function Zn(t,e,n){return En(t,e,n,"weekdaysMin")}function Jn(){var t=this._data;return this._milliseconds=Ja(this._milliseconds),this._days=Ja(this._days),this._months=Ja(this._months),t.milliseconds=Ja(t.milliseconds),t.seconds=Ja(t.seconds),t.minutes=Ja(t.minutes),t.hours=Ja(t.hours),t.months=Ja(t.months),t.years=Ja(t.years),this}function Xn(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 Kn(t,e){return Xn(this,t,e,1)}function Qn(t,e){return Xn(this,t,e,-1)}function $n(t){return 0>t?Math.floor(t):Math.ceil(t)}function ti(){var t,e,n,i,a,o=this._milliseconds,r=this._days,s=this._months,l=this._data;return o>=0&&r>=0&&s>=0||0>=o&&0>=r&&0>=s||(o+=864e5*$n(ni(s)+r),r=0,s=0),l.milliseconds=o%1e3,t=x(o/1e3),l.seconds=t%60,e=x(t/60),l.minutes=e%60,n=x(e/60),l.hours=n%24,r+=x(n/24),a=x(ei(r)),s+=a,r-=$n(ni(a)),i=x(s/12),s%=12,l.days=r,l.months=s,l.years=i,this}function ei(t){return 4800*t/146097}function ni(t){return 146097*t/4800}function ii(t){var e,n,i=this._milliseconds;if(t=V(t),"month"===t||"year"===t)return e=this._days+i/864e5,n=this._months+ei(e),"month"===t?n:n/12;switch(e=this._days+Math.round(ni(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 ai(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*k(this._months/12)}function oi(t){return function(){return this.as(t)}}function ri(t){return t=V(t),this[t+"s"]()}function si(t){return function(){return this._data[t]}}function li(){return x(this.days()/7)}function di(t,e,n,i,a){return a.relativeTime(e||1,!!n,t,i)}function ui(t,e,n){var i=He(t).abs(),a=ho(i.as("s")),o=ho(i.as("m")),r=ho(i.as("h")),s=ho(i.as("d")),l=ho(i.as("M")),d=ho(i.as("y")),u=a=o&&["m"]||o=r&&["h"]||r=s&&["d"]||s=l&&["M"]||l=d&&["y"]||["yy",d];return u[2]=e,u[3]=+t>0,u[4]=n,di.apply(null,u)}function ci(t){return void 0===t?ho:"function"==typeof t?(ho=t,!0):!1}function hi(t,e){return void 0===fo[t]?!1:void 0===e?fo[t]:(fo[t]=e,!0)}function fi(t){var e=this.localeData(),n=ui(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)}function gi(){var t,e,n,i=go(this._milliseconds)/1e3,a=go(this._days),o=go(this._months);t=x(i/60),e=x(t/60),i%=60,t%=60,n=x(o/12),o%=12;var r=n,s=o,l=a,d=e,u=t,c=i,h=this.asSeconds();return h?(0>h?"-":"")+"P"+(r?r+"Y":"")+(s?s+"M":"")+(l?l+"D":"")+(d||u||c?"T":"")+(d?d+"H":"")+(u?u+"M":"")+(c?c+"S":""):"P0D"}var mi,pi;pi=Array.prototype.some?Array.prototype.some:function(t){for(var e=Object(this),n=e.length>>>0,i=0;n>i;i++)if(i in e&&t.call(this,e[i],i,e))return!0;return!1};var vi=t.momentProperties=[],bi=!1,yi={};t.suppressDeprecationWarnings=!1,t.deprecationHandler=null;var xi;xi=Object.keys?Object.keys:function(t){var e,n=[];for(e in t)d(t,e)&&n.push(e);return n};var ki,Si={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},wi={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},_i="Invalid date",Mi="%d",Di=/\d{1,2}/,Ci={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Ti={},Pi={},Fi=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Ii=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Ai={},Oi={},Ri=/\d/,Wi=/\d\d/,Li=/\d{3}/,Vi=/\d{4}/,Bi=/[+-]?\d{6}/,Yi=/\d\d?/,zi=/\d\d\d\d?/,Hi=/\d\d\d\d\d\d?/,Ni=/\d{1,3}/,Ei=/\d{1,4}/,Ui=/[+-]?\d{1,6}/,ji=/\d+/,Gi=/[+-]?\d+/,qi=/Z|[+-]\d\d:?\d\d/gi,Zi=/Z|[+-]\d\d(?::?\d\d)?/gi,Ji=/[+-]?\d+(\.\d{1,3})?/,Xi=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Ki={},Qi={},$i=0,ta=1,ea=2,na=3,ia=4,aa=5,oa=6,ra=7,sa=8;ki=Array.prototype.indexOf?Array.prototype.indexOf:function(t){var e;for(e=0;e=t?""+t:"+"+t}),q(0,["YY",2],0,function(){return this.year()%100}),q(0,["YYYY",4],0,"year"),q(0,["YYYYY",5],0,"year"),q(0,["YYYYYY",6,!0],0,"year"),L("year","y"),Y("year",1),Q("Y",Gi),Q("YY",Yi,Wi),Q("YYYY",Ei,Vi),Q("YYYYY",Ui,Bi),Q("YYYYYY",Ui,Bi),nt(["YYYYY","YYYYYY"],$i),nt("YYYY",function(e,n){n[$i]=2===e.length?t.parseTwoDigitYear(e):k(e)}),nt("YY",function(e,n){n[$i]=t.parseTwoDigitYear(e)}),nt("Y",function(t,e){e[$i]=parseInt(t,10)}),t.parseTwoDigitYear=function(t){return k(t)+(k(t)>68?1900:2e3)};var fa=H("FullYear",!0);q("w",["ww",2],"wo","week"),q("W",["WW",2],"Wo","isoWeek"),L("week","w"),L("isoWeek","W"),Y("week",5),Y("isoWeek",5),Q("w",Yi),Q("ww",Yi,Wi),Q("W",Yi),Q("WW",Yi,Wi),it(["w","ww","W","WW"],function(t,e,n,i){e[i.substr(0,1)]=k(t)});var ga={dow:0,doy:6};q("d",0,"do","day"),q("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),q("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),q("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),q("e",0,0,"weekday"),q("E",0,0,"isoWeekday"),L("day","d"),L("weekday","e"),L("isoWeekday","E"),Y("day",11),Y("weekday",11),Y("isoWeekday",11),Q("d",Yi),Q("e",Yi),Q("E",Yi),Q("dd",function(t,e){return e.weekdaysMinRegex(t)}),Q("ddd",function(t,e){return e.weekdaysShortRegex(t)}),Q("dddd",function(t,e){return e.weekdaysRegex(t)}),it(["dd","ddd","dddd"],function(t,e,n,i){var a=n._locale.weekdaysParse(t,i,n._strict);null!=a?e.d=a:f(n).invalidWeekday=t}),it(["d","e","E"],function(t,e,n,i){e[i]=k(t)});var ma="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),pa="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),va="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),ba=Xi,ya=Xi,xa=Xi;q("H",["HH",2],0,"hour"),q("h",["hh",2],0,Ut),q("k",["kk",2],0,jt),q("hmm",0,0,function(){return""+Ut.apply(this)+G(this.minutes(),2)}),q("hmmss",0,0,function(){return""+Ut.apply(this)+G(this.minutes(),2)+G(this.seconds(),2)}),q("Hmm",0,0,function(){return""+this.hours()+G(this.minutes(),2)}),q("Hmmss",0,0,function(){return""+this.hours()+G(this.minutes(),2)+G(this.seconds(),2)}),Gt("a",!0),Gt("A",!1),L("hour","h"),Y("hour",13),Q("a",qt),Q("A",qt),Q("H",Yi),Q("h",Yi),Q("HH",Yi,Wi),Q("hh",Yi,Wi),Q("hmm",zi),Q("hmmss",Hi),Q("Hmm",zi),Q("Hmmss",Hi),nt(["H","HH"],na),nt(["a","A"],function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t}),nt(["h","hh"],function(t,e,n){e[na]=k(t),f(n).bigHour=!0}),nt("hmm",function(t,e,n){var i=t.length-2;e[na]=k(t.substr(0,i)),e[ia]=k(t.substr(i)),f(n).bigHour=!0}),nt("hmmss",function(t,e,n){var i=t.length-4,a=t.length-2;e[na]=k(t.substr(0,i)),e[ia]=k(t.substr(i,2)),e[aa]=k(t.substr(a)),f(n).bigHour=!0}),nt("Hmm",function(t,e,n){var i=t.length-2;e[na]=k(t.substr(0,i)),e[ia]=k(t.substr(i))}),nt("Hmmss",function(t,e,n){var i=t.length-4,a=t.length-2;e[na]=k(t.substr(0,i)),e[ia]=k(t.substr(i,2)),e[aa]=k(t.substr(a))});var ka,Sa=/[ap]\.?m?\.?/i,wa=H("Hours",!0),_a={calendar:Si,longDateFormat:wi,invalidDate:_i,ordinal:Mi,ordinalParse:Di,relativeTime:Ci,months:da,monthsShort:ua,week:ga,weekdays:ma,weekdaysMin:va,weekdaysShort:pa,meridiemParse:Sa},Ma={},Da=/^\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)?)?/,Ca=/^\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)?)?/,Ta=/Z|[+-]\d\d(?::?\d\d)?/,Pa=[["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}/]],Fa=[["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/]],Ia=/^\/?Date\((\-?\d+)/i; +t.createFromInputFallback=_("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 Aa=_("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var t=ye.apply(null,arguments);return this.isValid()&&t.isValid()?this>t?this:t:m()}),Oa=_("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var t=ye.apply(null,arguments);return this.isValid()&&t.isValid()?t>this?this:t:m()}),Ra=function(){return Date.now?Date.now():+new Date};De("Z",":"),De("ZZ",""),Q("Z",Zi),Q("ZZ",Zi),nt(["Z","ZZ"],function(t,e,n){n._useUTC=!0,n._tzm=Ce(Zi,t)});var Wa=/([\+\-]|\d\d)/gi;t.updateOffset=function(){};var La=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Va=/^(-)?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=we.prototype;var Ba=je(1,"add"),Ya=je(-1,"subtract");t.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",t.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var za=_("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)});q(0,["gg",2],0,function(){return this.weekYear()%100}),q(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Dn("gggg","weekYear"),Dn("ggggg","weekYear"),Dn("GGGG","isoWeekYear"),Dn("GGGGG","isoWeekYear"),L("weekYear","gg"),L("isoWeekYear","GG"),Y("weekYear",1),Y("isoWeekYear",1),Q("G",Gi),Q("g",Gi),Q("GG",Yi,Wi),Q("gg",Yi,Wi),Q("GGGG",Ei,Vi),Q("gggg",Ei,Vi),Q("GGGGG",Ui,Bi),Q("ggggg",Ui,Bi),it(["gggg","ggggg","GGGG","GGGGG"],function(t,e,n,i){e[i.substr(0,2)]=k(t)}),it(["gg","GG"],function(e,n,i,a){n[a]=t.parseTwoDigitYear(e)}),q("Q",0,"Qo","quarter"),L("quarter","Q"),Y("quarter",7),Q("Q",Ri),nt("Q",function(t,e){e[ta]=3*(k(t)-1)}),q("D",["DD",2],"Do","date"),L("date","D"),Y("date",9),Q("D",Yi),Q("DD",Yi,Wi),Q("Do",function(t,e){return t?e._ordinalParse:e._ordinalParseLenient}),nt(["D","DD"],ea),nt("Do",function(t,e){e[ea]=k(t.match(Yi)[0],10)});var Ha=H("Date",!0);q("DDD",["DDDD",3],"DDDo","dayOfYear"),L("dayOfYear","DDD"),Y("dayOfYear",4),Q("DDD",Ni),Q("DDDD",Li),nt(["DDD","DDDD"],function(t,e,n){n._dayOfYear=k(t)}),q("m",["mm",2],0,"minute"),L("minute","m"),Y("minute",14),Q("m",Yi),Q("mm",Yi,Wi),nt(["m","mm"],ia);var Na=H("Minutes",!1);q("s",["ss",2],0,"second"),L("second","s"),Y("second",15),Q("s",Yi),Q("ss",Yi,Wi),nt(["s","ss"],aa);var Ea=H("Seconds",!1);q("S",0,0,function(){return~~(this.millisecond()/100)}),q(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),q(0,["SSS",3],0,"millisecond"),q(0,["SSSS",4],0,function(){return 10*this.millisecond()}),q(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),q(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),q(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),q(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),q(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),L("millisecond","ms"),Y("millisecond",16),Q("S",Ni,Ri),Q("SS",Ni,Wi),Q("SSS",Ni,Li);var Ua;for(Ua="SSSS";Ua.length<=9;Ua+="S")Q(Ua,ji);for(Ua="S";Ua.length<=9;Ua+="S")nt(Ua,Wn);var ja=H("Milliseconds",!1);q("z",0,0,"zoneAbbr"),q("zz",0,0,"zoneName");var Ga=b.prototype;Ga.add=Ba,Ga.calendar=Ze,Ga.clone=Je,Ga.diff=nn,Ga.endOf=mn,Ga.format=sn,Ga.from=ln,Ga.fromNow=dn,Ga.to=un,Ga.toNow=cn,Ga.get=U,Ga.invalidAt=_n,Ga.isAfter=Xe,Ga.isBefore=Ke,Ga.isBetween=Qe,Ga.isSame=$e,Ga.isSameOrAfter=tn,Ga.isSameOrBefore=en,Ga.isValid=Sn,Ga.lang=za,Ga.locale=hn,Ga.localeData=fn,Ga.max=Oa,Ga.min=Aa,Ga.parsingFlags=wn,Ga.set=j,Ga.startOf=gn,Ga.subtract=Ya,Ga.toArray=yn,Ga.toObject=xn,Ga.toDate=bn,Ga.toISOString=rn,Ga.toJSON=kn,Ga.toString=on,Ga.unix=vn,Ga.valueOf=pn,Ga.creationData=Mn,Ga.year=fa,Ga.isLeapYear=bt,Ga.weekYear=Cn,Ga.isoWeekYear=Tn,Ga.quarter=Ga.quarters=On,Ga.month=ct,Ga.daysInMonth=ht,Ga.week=Ga.weeks=Tt,Ga.isoWeek=Ga.isoWeeks=Pt,Ga.weeksInYear=Fn,Ga.isoWeeksInYear=Pn,Ga.date=Ha,Ga.day=Ga.days=Vt,Ga.weekday=Bt,Ga.isoWeekday=Yt,Ga.dayOfYear=Rn,Ga.hour=Ga.hours=wa,Ga.minute=Ga.minutes=Na,Ga.second=Ga.seconds=Ea,Ga.millisecond=Ga.milliseconds=ja,Ga.utcOffset=Fe,Ga.utc=Ae,Ga.local=Oe,Ga.parseZone=Re,Ga.hasAlignedHourOffset=We,Ga.isDST=Le,Ga.isLocal=Be,Ga.isUtcOffset=Ye,Ga.isUtc=ze,Ga.isUTC=ze,Ga.zoneAbbr=Ln,Ga.zoneName=Vn,Ga.dates=_("dates accessor is deprecated. Use date instead.",Ha),Ga.months=_("months accessor is deprecated. Use month instead",ct),Ga.years=_("years accessor is deprecated. Use year instead",fa),Ga.zone=_("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Ie),Ga.isDSTShifted=_("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Ve);var qa=Ga,Za=P.prototype;Za.calendar=F,Za.longDateFormat=I,Za.invalidDate=A,Za.ordinal=O,Za.preparse=zn,Za.postformat=zn,Za.relativeTime=R,Za.pastFuture=W,Za.set=C,Za.months=rt,Za.monthsShort=st,Za.monthsParse=dt,Za.monthsRegex=gt,Za.monthsShortRegex=ft,Za.week=Mt,Za.firstDayOfYear=Ct,Za.firstDayOfWeek=Dt,Za.weekdays=At,Za.weekdaysMin=Rt,Za.weekdaysShort=Ot,Za.weekdaysParse=Lt,Za.weekdaysRegex=zt,Za.weekdaysShortRegex=Ht,Za.weekdaysMinRegex=Nt,Za.isPM=Zt,Za.meridiem=Jt,$t("en",{ordinalParse:/\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=_("moment.lang is deprecated. Use moment.locale instead.",$t),t.langData=_("moment.langData is deprecated. Use moment.localeData instead.",ne);var Ja=Math.abs,Xa=oi("ms"),Ka=oi("s"),Qa=oi("m"),$a=oi("h"),to=oi("d"),eo=oi("w"),no=oi("M"),io=oi("y"),ao=si("milliseconds"),oo=si("seconds"),ro=si("minutes"),so=si("hours"),lo=si("days"),uo=si("months"),co=si("years"),ho=Math.round,fo={s:45,m:45,h:22,d:26,M:11},go=Math.abs,mo=we.prototype;mo.abs=Jn,mo.add=Kn,mo.subtract=Qn,mo.as=ii,mo.asMilliseconds=Xa,mo.asSeconds=Ka,mo.asMinutes=Qa,mo.asHours=$a,mo.asDays=to,mo.asWeeks=eo,mo.asMonths=no,mo.asYears=io,mo.valueOf=ai,mo._bubble=ti,mo.get=ri,mo.milliseconds=ao,mo.seconds=oo,mo.minutes=ro,mo.hours=so,mo.days=lo,mo.weeks=li,mo.months=uo,mo.years=co,mo.humanize=fi,mo.toISOString=gi,mo.toString=gi,mo.toJSON=gi,mo.locale=hn,mo.localeData=fn,mo.toIsoString=_("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",gi),mo.lang=za,q("X",0,0,"unix"),q("x",0,0,"valueOf"),Q("x",Gi),Q("X",Ji),nt("X",function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))}),nt("x",function(t,e,n){n._d=new Date(k(t))}),t.version="2.15.1",i(ye),t.fn=qa,t.min=ke,t.max=Se,t.now=Ra,t.utc=c,t.unix=Bn,t.months=Un,t.isDate=s,t.locale=$t,t.invalid=m,t.duration=He,t.isMoment=y,t.weekdays=Gn,t.parseZone=Yn,t.localeData=ne,t.isDuration=_e,t.monthsShort=jn,t.weekdaysMin=Zn,t.defineLocale=te,t.updateLocale=ee,t.locales=ie,t.weekdaysShort=qn,t.normalizeUnits=V,t.relativeTimeRounding=ci,t.relativeTimeThreshold=hi,t.calendarFormat=qe,t.prototype=qa;var po=t;return po})},{}],7:[function(t,e,n){var i=t(27)();t(26)(i),t(22)(i),t(25)(i),t(21)(i),t(23)(i),t(24)(i),t(28)(i),t(32)(i),t(30)(i),t(31)(i),t(33)(i),t(29)(i),t(34)(i),t(35)(i),t(36)(i),t(37)(i),t(38)(i),t(41)(i),t(39)(i),t(40)(i),t(42)(i),t(43)(i),t(44)(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,41:41,42:42,43:43,44:44,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.options=t.helpers.configMerge({aspectRatio:1},n.options),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),this.getMeta().bar=!0},getBarCount:function(){var t=this,n=0;return e.each(t.chart.data.datasets,function(e,i){var a=t.chart.getDatasetMeta(i);a.bar&&t.chart.isDatasetVisible(i)&&++n},t),n},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,o=a.getMeta(),r=a.getScaleForId(o.xAxisID),s=a.getScaleForId(o.yAxisID),l=s.getBasePixel(),d=a.chart.options.elements.rectangle,u=t.custom||{},c=a.getDataset();e.extend(t,{_xScale:r,_yScale:s,_datasetIndex:a.index,_index:n,_model:{x:a.calculateBarX(n,a.index),y:i?l:a.calculateBarY(n,a.index),label:a.chart.data.labels[n],datasetLabel:c.label,base:i?l:a.calculateBarBase(a.index,n),width:a.calculateBarWidth(n),backgroundColor:u.backgroundColor?u.backgroundColor:e.getValueAtIndexOrDefault(c.backgroundColor,n,d.backgroundColor),borderSkipped:u.borderSkipped?u.borderSkipped:d.borderSkipped,borderColor:u.borderColor?u.borderColor:e.getValueAtIndexOrDefault(c.borderColor,n,d.borderColor),borderWidth:u.borderWidth?u.borderWidth:e.getValueAtIndexOrDefault(c.borderWidth,n,d.borderWidth)}}),t.pivot()},calculateBarBase:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.yAxisID),o=0;if(a.options.stacked){for(var r=n.chart,s=r.data.datasets,l=Number(s[t].data[e]),d=0;t>d;d++){var u=s[d],c=r.getDatasetMeta(d);if(c.bar&&c.yAxisID===a.id&&r.isDatasetVisible(d)){var h=Number(u.data[e]);o+=0>l?Math.min(h,0):Math.max(h,0)}}return a.getPixelForValue(o)}return a.getBasePixel()},getRuler:function(t){var e,n=this,i=n.getMeta(),a=n.getScaleForId(i.xAxisID),o=n.getBarCount();e="category"===a.options.type?a.getPixelForTick(t+1)-a.getPixelForTick(t):a.width/a.ticks.length;var r=e*a.options.categoryPercentage,s=(e-e*a.options.categoryPercentage)/2,l=r/o;if(a.ticks.length!==n.chart.data.labels.length){var d=a.ticks.length/n.chart.data.labels.length;l*=d}var u=l*a.options.barPercentage,c=l-l*a.options.barPercentage;return{datasetCount:o,tickWidth:e,categoryWidth:r,categorySpacing:s,fullBarWidth:l,barWidth:u,barSpacing:c}},calculateBarWidth:function(t){var e=this.getScaleForId(this.getMeta().xAxisID);if(e.options.barThickness)return e.options.barThickness;var n=this.getRuler(t);return e.options.stacked?n.categoryWidth:n.barWidth},getBarIndex:function(t){var e,n,i=0;for(n=0;t>n;++n)e=this.chart.getDatasetMeta(n),e.bar&&this.chart.isDatasetVisible(n)&&++i;return i},calculateBarX:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.xAxisID),o=n.getBarIndex(e),r=n.getRuler(t),s=a.getPixelForValue(null,t,e,n.chart.isCombo);return s-=n.chart.isCombo?r.tickWidth/2:0,a.options.stacked?s+r.categoryWidth/2+r.categorySpacing:s+r.barWidth/2+r.categorySpacing+r.barWidth*o+r.barSpacing/2+r.barSpacing*o},calculateBarY:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.yAxisID),o=Number(n.getDataset().data[t]);if(a.options.stacked){for(var r=0,s=0,l=0;e>l;l++){var d=n.chart.data.datasets[l],u=n.chart.getDatasetMeta(l);if(u.bar&&u.yAxisID===a.id&&n.chart.isDatasetVisible(l)){var c=Number(d.data[t]);0>c?s+=c||0:r+=c||0}}return 0>o?a.getPixelForValue(s+o):a.getPixelForValue(r+o)}return a.getPixelForValue(o)},draw:function(t){var n=this,i=t||1;e.each(n.getMeta().data,function(t,e){var a=n.getDataset().data[e];null===a||void 0===a||isNaN(a)||t.transition(i).draw()},n)},setHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t._index,a=t.custom||{},o=t._model;o.backgroundColor=a.hoverBackgroundColor?a.hoverBackgroundColor:e.getValueAtIndexOrDefault(n.hoverBackgroundColor,i,e.getHoverColor(o.backgroundColor)),o.borderColor=a.hoverBorderColor?a.hoverBorderColor:e.getValueAtIndexOrDefault(n.hoverBorderColor,i,e.getHoverColor(o.borderColor)),o.borderWidth=a.hoverBorderWidth?a.hoverBorderWidth:e.getValueAtIndexOrDefault(n.hoverBorderWidth,i,o.borderWidth)},removeHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t._index,a=t.custom||{},o=t._model,r=this.chart.options.elements.rectangle;o.backgroundColor=a.backgroundColor?a.backgroundColor:e.getValueAtIndexOrDefault(n.backgroundColor,i,r.backgroundColor),o.borderColor=a.borderColor?a.borderColor:e.getValueAtIndexOrDefault(n.borderColor,i,r.borderColor),o.borderWidth=a.borderWidth?a.borderWidth:e.getValueAtIndexOrDefault(n.borderWidth,i,r.borderWidth)}}),t.defaults.horizontalBar={hover:{mode:"label"},scales:{xAxes:[{type:"linear",position:"bottom"}],yAxes:[{position:"left",type:"category",categoryPercentage:.8,barPercentage:.9,gridLines:{offsetGridLines:!0}}]},elements:{rectangle:{borderSkipped:"left"}},tooltips:{callbacks:{title:function(t,e){var n="";return t.length>0&&(t[0].yLabel?n=t[0].yLabel:e.labels.length>0&&t[0].indexc;c++)e.lineTo.apply(e,t(c));e.fill(),n.borderWidth&&e.stroke()},inRange:function(t,e){var n=this._view,i=!1;return n&&(i=n.x=n.y-n.height/2&&e<=n.y+n.height/2&&t>=n.x&&t<=n.base:e>=n.y-n.height/2&&e<=n.y+n.height/2&&t>=n.base&&t<=n.x),i}}),t.pivot()},calculateBarBase:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.xAxisID),o=0;if(a.options.stacked){for(var r=n.chart,s=r.data.datasets,l=Number(s[t].data[e]),d=0;t>d;d++){var u=s[d],c=r.getDatasetMeta(d);if(c.bar&&c.xAxisID===a.id&&r.isDatasetVisible(d)){var h=Number(u.data[e]);o+=0>l?Math.min(h,0):Math.max(h,0)}}return a.getPixelForValue(o)}return a.getBasePixel()},getRuler:function(t){var e,n=this,i=n.getMeta(),a=n.getScaleForId(i.yAxisID),o=n.getBarCount();e="category"===a.options.type?a.getPixelForTick(t+1)-a.getPixelForTick(t):a.width/a.ticks.length;var r=e*a.options.categoryPercentage,s=(e-e*a.options.categoryPercentage)/2,l=r/o;if(a.ticks.length!==n.chart.data.labels.length){var d=a.ticks.length/n.chart.data.labels.length;l*=d}var u=l*a.options.barPercentage,c=l-l*a.options.barPercentage;return{datasetCount:o,tickHeight:e,categoryHeight:r,categorySpacing:s,fullBarHeight:l,barHeight:u,barSpacing:c}},calculateBarHeight:function(t){var e=this,n=e.getScaleForId(e.getMeta().yAxisID);if(n.options.barThickness)return n.options.barThickness;var i=e.getRuler(t);return n.options.stacked?i.categoryHeight:i.barHeight},calculateBarX:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.xAxisID),o=Number(n.getDataset().data[t]);if(a.options.stacked){for(var r=0,s=0,l=0;e>l;l++){var d=n.chart.data.datasets[l],u=n.chart.getDatasetMeta(l);if(u.bar&&u.xAxisID===a.id&&n.chart.isDatasetVisible(l)){var c=Number(d.data[t]);0>c?s+=c||0:r+=c||0}}return 0>o?a.getPixelForValue(s+o):a.getPixelForValue(r+o)}return a.getPixelForValue(o)},calculateBarY:function(t,e){var n=this,i=n.getMeta(),a=n.getScaleForId(i.yAxisID),o=n.getBarIndex(e),r=n.getRuler(t),s=a.getPixelForValue(null,t,e,n.chart.isCombo);return s-=n.chart.isCombo?r.tickHeight/2:0,a.options.stacked?s+r.categoryHeight/2+r.categorySpacing:s+r.barHeight/2+r.categorySpacing+r.barHeight*o+r.barSpacing/2+r.barSpacing*o}})}},{}],16:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.bubble={hover:{mode:"single"},scales:{xAxes:[{type:"linear",position:"bottom",id:"x-axis-0"}],yAxes:[{type:"linear",position:"left",id:"y-axis-0"}]},tooltips:{callbacks:{title:function(){return""},label:function(t,e){var n=e.datasets[t.datasetIndex].label||"",i=e.datasets[t.datasetIndex].data[t.index];return n+": ("+i.x+", "+i.y+", "+i.r+")"}}}},t.controllers.bubble=t.DatasetController.extend({dataElementType:t.elements.Point,update:function(t){var n=this,i=n.getMeta(),a=i.data;e.each(a,function(e,i){n.updateElement(e,i,t)})},updateElement:function(n,i,a){var o=this,r=o.getMeta(),s=o.getScaleForId(r.xAxisID),l=o.getScaleForId(r.yAxisID),d=n.custom||{},u=o.getDataset(),c=u.data[i],h=o.chart.options.elements.point,f=o.index;e.extend(n,{_xScale:s,_yScale:l,_datasetIndex:f,_index:i,_model:{x:a?s.getPixelForDecimal(.5):s.getPixelForValue("object"==typeof c?c:NaN,i,f,o.chart.isCombo),y:a?l.getBasePixel():l.getPixelForValue(c,i,f),radius:a?0:d.radius?d.radius:o.getRadius(c),hitRadius:d.hitRadius?d.hitRadius:e.getValueAtIndexOrDefault(u.hitRadius,i,h.hitRadius)}}),t.DatasetController.prototype.removeHoverStyle.call(o,n,h);var g=n._model;g.skip=d.skip?d.skip:isNaN(g.x)||isNaN(g.y),n.pivot()},getRadius:function(t){return t.r||this.chart.options.elements.point.radius},setHoverStyle:function(n){var i=this;t.DatasetController.prototype.setHoverStyle.call(i,n);var a=i.chart.data.datasets[n._datasetIndex],o=n._index,r=n.custom||{},s=n._model;s.radius=r.hoverRadius?r.hoverRadius:e.getValueAtIndexOrDefault(a.hoverRadius,o,i.chart.options.elements.point.hoverRadius)+i.getRadius(a.data[o])},removeHoverStyle:function(e){var n=this;t.DatasetController.prototype.removeHoverStyle.call(n,e,n.chart.options.elements.point);var i=n.chart.data.datasets[e._datasetIndex].data[e._index],a=e.custom||{},o=e._model;o.radius=a.radius?a.radius:n.getRadius(i)}})}},{}],17:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=t.defaults;n.doughnut={animation:{animateRotate:!0,animateScale:!1},aspectRatio:1,hover:{mode:"single"},legendCallback:function(t){var e=[];e.push('
    ');var n=t.data,i=n.datasets,a=n.labels;if(i.length)for(var o=0;o'),a[o]&&e.push(a[o]),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 o=t.getDatasetMeta(0),r=n.datasets[0],s=o.data[a],l=s&&s.custom||{},d=e.getValueAtIndexOrDefault,u=t.options.elements.arc,c=l.backgroundColor?l.backgroundColor:d(r.backgroundColor,a,u.backgroundColor),h=l.borderColor?l.borderColor:d(r.borderColor,a,u.borderColor),f=l.borderWidth?l.borderWidth:d(r.borderWidth,a,u.borderWidth);return{text:i,fillStyle:c,strokeStyle:h,lineWidth:f,hidden:isNaN(r.data[a])||o.data[a].hidden,index:a}}):[]}},onClick:function(t,e){var n,i,a,o=e.index,r=this.chart;for(n=0,i=(r.data.datasets||[]).length;i>n;++n)a=r.getDatasetMeta(n),a.data[o]&&(a.data[o].hidden=!a.data[o].hidden);r.update()}},cutoutPercentage:50,rotation:Math.PI*-.5,circumference:2*Math.PI,tooltips:{callbacks:{title:function(){return""},label:function(t,e){return e.labels[t.index]+": "+e.datasets[t.datasetIndex].data[t.index]}}}},n.pie=e.clone(n.doughnut),e.extend(n.pie,{cutoutPercentage:0}),t.controllers.doughnut=t.controllers.pie=t.DatasetController.extend({dataElementType:t.elements.Arc,linkScales:e.noop,getRingIndex:function(t){for(var e=0,n=0;t>n;++n)this.chart.isDatasetVisible(n)&&++e;return e},update:function(t){var n=this,i=n.chart,a=i.chartArea,o=i.options,r=o.elements.arc,s=a.right-a.left-r.borderWidth,l=a.bottom-a.top-r.borderWidth,d=Math.min(s,l),u={x:0,y:0},c=n.getMeta(),h=o.cutoutPercentage,f=o.circumference;if(f<2*Math.PI){var g=o.rotation%(2*Math.PI);g+=2*Math.PI*(g>=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=0>=g&&m>=0||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,S=h/100,w={x:x?-1:Math.min(p.x*(p.x<0?1:S),v.x*(v.x<0?1:S)),y:k?-1:Math.min(p.y*(p.y<0?1:S),v.y*(v.y<0?1:S))},_={x:b?1:Math.max(p.x*(p.x>0?1:S),v.x*(v.x>0?1:S)),y:y?1:Math.max(p.y*(p.y>0?1:S),v.y*(v.y>0?1:S))},M={width:.5*(_.x-w.x),height:.5*(_.y-w.y)};d=Math.min(s/M.width,l/M.height),u={x:(_.x+w.x)*-.5,y:(_.y+w.y)*-.5}}i.borderWidth=n.getMaxBorderWidth(c.data),i.outerRadius=Math.max((d-i.borderWidth)/2,0),i.innerRadius=Math.max(h?i.outerRadius/100*h:1,0),i.radiusLength=(i.outerRadius-i.innerRadius)/i.getVisibleDatasetCount(),i.offsetX=u.x*i.outerRadius,i.offsetY=u.y*i.outerRadius,c.total=n.calculateTotal(),n.outerRadius=i.outerRadius-i.radiusLength*n.getRingIndex(n.index),n.innerRadius=n.outerRadius-i.radiusLength,e.each(c.data,function(e,i){n.updateElement(e,i,t)})},updateElement:function(t,n,i){var a=this,o=a.chart,r=o.chartArea,s=o.options,l=s.animation,d=(r.left+r.right)/2,u=(r.top+r.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:d+o.offsetX,y:u+o.offsetY,startAngle:c,endAngle:h,circumference:g,outerRadius:p,innerRadius:m,label:v(f.label,n,o.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,o=t.length,r=0;o>r;r++)e=t[r]._model?t[r]._model.borderWidth:0,n=t[r]._chart?t[r]._chart.config.data.datasets[a].hoverBorderWidth:0,i=e>i?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,addElementAndReset:function(n){var i=this,a=i.chart.options,o=i.getMeta();t.DatasetController.prototype.addElementAndReset.call(i,n),e(i.getDataset(),a)&&0!==o.dataset._model.tension&&i.updateBezierControlPoints()},update:function(t){var i,a,o,r=this,s=r.getMeta(),l=s.dataset,d=s.data||[],u=r.chart.options,c=u.elements.line,h=r.getScaleForId(s.yAxisID),f=r.getDataset(),g=e(f,u);for(g&&(o=l.custom||{},void 0!==f.tension&&void 0===f.lineTension&&(f.lineTension=f.tension),l._scale=h,l._datasetIndex=r.index,l._children=d,l._model={spanGaps:f.spanGaps?f.spanGaps:u.spanGaps,tension:o.tension?o.tension:n.getValueOrDefault(f.lineTension,c.tension),backgroundColor:o.backgroundColor?o.backgroundColor:f.backgroundColor||c.backgroundColor,borderWidth:o.borderWidth?o.borderWidth:f.borderWidth||c.borderWidth,borderColor:o.borderColor?o.borderColor:f.borderColor||c.borderColor,borderCapStyle:o.borderCapStyle?o.borderCapStyle:f.borderCapStyle||c.borderCapStyle,borderDash:o.borderDash?o.borderDash:f.borderDash||c.borderDash,borderDashOffset:o.borderDashOffset?o.borderDashOffset:f.borderDashOffset||c.borderDashOffset,borderJoinStyle:o.borderJoinStyle?o.borderJoinStyle:f.borderJoinStyle||c.borderJoinStyle,fill:o.fill?o.fill:void 0!==f.fill?f.fill:c.fill,steppedLine:o.steppedLine?o.steppedLine:n.getValueOrDefault(f.steppedLine,c.stepped),cubicInterpolationMode:o.cubicInterpolationMode?o.cubicInterpolationMode:n.getValueOrDefault(f.cubicInterpolationMode,c.cubicInterpolationMode),scaleTop:h.top,scaleBottom:h.bottom,scaleZero:h.getBasePixel()},l.pivot()),i=0,a=d.length;a>i;++i)r.updateElement(d[i],i,t);for(g&&0!==l._model.tension&&r.updateBezierControlPoints(),i=0,a=d.length;a>i;++i)d[i].pivot()},getPointBackgroundColor:function(t,e){var i=this.chart.options.elements.point.backgroundColor,a=this.getDataset(),o=t.custom||{};return o.backgroundColor?i=o.backgroundColor:a.pointBackgroundColor?i=n.getValueAtIndexOrDefault(a.pointBackgroundColor,e,i):a.backgroundColor&&(i=a.backgroundColor),i},getPointBorderColor:function(t,e){var i=this.chart.options.elements.point.borderColor,a=this.getDataset(),o=t.custom||{};return o.borderColor?i=o.borderColor:a.pointBorderColor?i=n.getValueAtIndexOrDefault(a.pointBorderColor,e,i):a.borderColor&&(i=a.borderColor),i},getPointBorderWidth:function(t,e){var i=this.chart.options.elements.point.borderWidth,a=this.getDataset(),o=t.custom||{};return o.borderWidth?i=o.borderWidth:a.pointBorderWidth?i=n.getValueAtIndexOrDefault(a.pointBorderWidth,e,i):a.borderWidth&&(i=a.borderWidth),i},updateElement:function(t,e,i){var a,o,r=this,s=r.getMeta(),l=t.custom||{},d=r.getDataset(),u=r.index,c=d.data[e],h=r.getScaleForId(s.yAxisID),f=r.getScaleForId(s.xAxisID),g=r.chart.options.elements.point,m=r.chart.data.labels||[],p=1===m.length||1===d.data.length||r.chart.isCombo;void 0!==d.radius&&void 0===d.pointRadius&&(d.pointRadius=d.radius),void 0!==d.hitRadius&&void 0===d.pointHitRadius&&(d.pointHitRadius=d.hitRadius),a=f.getPixelForValue("object"==typeof c?c:NaN,e,u,p),o=i?h.getBasePixel():r.calculatePointY(c,e,u),t._xScale=f,t._yScale=h,t._datasetIndex=u,t._index=e,t._model={x:a,y:o,skip:l.skip||isNaN(a)||isNaN(o),radius:l.radius||n.getValueAtIndexOrDefault(d.pointRadius,e,g.radius),pointStyle:l.pointStyle||n.getValueAtIndexOrDefault(d.pointStyle,e,g.pointStyle),backgroundColor:r.getPointBackgroundColor(t,e),borderColor:r.getPointBorderColor(t,e),borderWidth:r.getPointBorderWidth(t,e),tension:s.dataset._model?s.dataset._model.tension:0,steppedLine:s.dataset._model?s.dataset._model.steppedLine:!1,hitRadius:l.hitRadius||n.getValueAtIndexOrDefault(d.pointHitRadius,e,g.hitRadius)}},calculatePointY:function(t,e,n){var i,a,o,r=this,s=r.chart,l=r.getMeta(),d=r.getScaleForId(l.yAxisID),u=0,c=0;if(d.options.stacked){for(i=0;n>i;i++)if(a=s.data.datasets[i],o=s.getDatasetMeta(i),"line"===o.type&&o.yAxisID===d.id&&s.isDatasetVisible(i)){var h=Number(d.getRightValue(a.data[e]));0>h?c+=h||0:u+=h||0}var f=Number(d.getRightValue(t));return 0>f?d.getPixelForValue(c+f):d.getPixelForValue(u+f)}return d.getPixelForValue(t)},updateBezierControlPoints:function(){function t(t,e,n){return Math.max(Math.min(t,n),e)}var e,i,a,o,r,s=this,l=s.getMeta(),d=s.chart.chartArea,u=l.data||[];if(l.dataset._model.spanGaps&&(u=u.filter(function(t){return!t._model.skip})),"monotone"===l.dataset._model.cubicInterpolationMode)n.splineCurveMonotone(u);else for(e=0,i=u.length;i>e;++e)a=u[e],o=a._model,r=n.splineCurve(n.previousItem(u,e)._model,o,n.nextItem(u,e)._model,l.dataset._model.tension),o.controlPointPreviousX=r.previous.x,o.controlPointPreviousY=r.previous.y,o.controlPointNextX=r.next.x,o.controlPointNextY=r.next.y;if(s.chart.options.elements.line.capBezierPoints)for(e=0,i=u.length;i>e;++e)o=u[e]._model,o.controlPointPreviousX=t(o.controlPointPreviousX,d.left,d.right),o.controlPointPreviousY=t(o.controlPointPreviousY,d.top,d.bottom),o.controlPointNextX=t(o.controlPointNextX,d.left,d.right),o.controlPointNextY=t(o.controlPointNextY,d.top,d.bottom)},draw:function(t){var n,i,a=this,o=a.getMeta(),r=o.data||[],s=t||1;for(n=0,i=r.length;i>n;++n)r[n].transition(s);for(e(a.getDataset(),a.chart.options)&&o.dataset.transition(s).draw(),n=0,i=r.length;i>n;++n)r[n].draw()},setHoverStyle:function(t){var e=this.chart.data.datasets[t._datasetIndex],i=t._index,a=t.custom||{},o=t._model;o.radius=a.hoverRadius||n.getValueAtIndexOrDefault(e.pointHoverRadius,i,this.chart.options.elements.point.hoverRadius),o.backgroundColor=a.hoverBackgroundColor||n.getValueAtIndexOrDefault(e.pointHoverBackgroundColor,i,n.getHoverColor(o.backgroundColor)),o.borderColor=a.hoverBorderColor||n.getValueAtIndexOrDefault(e.pointHoverBorderColor,i,n.getHoverColor(o.borderColor)),o.borderWidth=a.hoverBorderWidth||n.getValueAtIndexOrDefault(e.pointHoverBorderWidth,i,o.borderWidth)},removeHoverStyle:function(t){var e=this,i=e.chart.data.datasets[t._datasetIndex],a=t._index,o=t.custom||{},r=t._model;void 0!==i.radius&&void 0===i.pointRadius&&(i.pointRadius=i.radius),r.radius=o.radius||n.getValueAtIndexOrDefault(i.pointRadius,a,e.chart.options.elements.point.radius),r.backgroundColor=e.getPointBackgroundColor(t,a),r.borderColor=e.getPointBorderColor(t,a),r.borderWidth=e.getPointBorderWidth(t,a)}})}},{}],19:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.polarArea={scale:{type:"radialLinear",lineArc:!0,ticks:{beginAtZero:!0}},animation:{animateRotate:!0,animateScale:!0},startAngle:-.5*Math.PI,aspectRatio:1,legendCallback:function(t){var e=[];e.push('
    ');var n=t.data,i=n.datasets,a=n.labels;if(i.length)for(var o=0;o'),a[o]&&e.push(a[o]),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 o=t.getDatasetMeta(0),r=n.datasets[0],s=o.data[a],l=s.custom||{},d=e.getValueAtIndexOrDefault,u=t.options.elements.arc,c=l.backgroundColor?l.backgroundColor:d(r.backgroundColor,a,u.backgroundColor),h=l.borderColor?l.borderColor:d(r.borderColor,a,u.borderColor),f=l.borderWidth?l.borderWidth:d(r.borderWidth,a,u.borderWidth);return{ +text:i,fillStyle:c,strokeStyle:h,lineWidth:f,hidden:isNaN(r.data[a])||o.data[a].hidden,index:a}}):[]}},onClick:function(t,e){var n,i,a,o=e.index,r=this.chart;for(n=0,i=(r.data.datasets||[]).length;i>n;++n)a=r.getDatasetMeta(n),a.data[o].hidden=!a.data[o].hidden;r.update()}},tooltips:{callbacks:{title:function(){return""},label:function(t,e){return e.labels[t.index]+": "+t.yLabel}}}},t.controllers.polarArea=t.DatasetController.extend({dataElementType:t.elements.Arc,linkScales:e.noop,update:function(t){var n=this,i=n.chart,a=i.chartArea,o=n.getMeta(),r=i.options,s=r.elements.arc,l=Math.min(a.right-a.left,a.bottom-a.top);i.outerRadius=Math.max((l-s.borderWidth/2)/2,0),i.innerRadius=Math.max(r.cutoutPercentage?i.outerRadius/100*r.cutoutPercentage:1,0),i.radiusLength=(i.outerRadius-i.innerRadius)/i.getVisibleDatasetCount(),n.outerRadius=i.outerRadius-i.radiusLength*n.index,n.innerRadius=n.outerRadius-i.radiusLength,o.count=n.countVisibleElements(),e.each(o.data,function(e,i){n.updateElement(e,i,t)})},updateElement:function(t,n,i){for(var a=this,o=a.chart,r=a.getDataset(),s=o.options,l=s.animation,d=o.scale,u=e.getValueAtIndexOrDefault,c=o.data.labels,h=a.calculateCircumference(r.data[n]),f=d.xCenter,g=d.yCenter,m=0,p=a.getMeta(),v=0;n>v;++v)isNaN(r.data[v])||p.data[v].hidden||++m;var b=s.startAngle,y=t.hidden?0:d.getDistanceFromCenterForValue(r.data[n]),x=b+h*m,k=x+(t.hidden?0:h),S=l.animateScale?0:d.getDistanceFromCenterForValue(r.data[n]);e.extend(t,{_datasetIndex:a.index,_index:n,_scale:d,_model:{x:f,y:g,innerRadius:0,outerRadius:i?S:y,startAngle:i&&l.animateRotate?b:x,endAngle:i&&l.animateRotate?b:k,label:u(c,n,c[n])}}),a.removeHoverStyle(t),t.pivot()},removeHoverStyle:function(e){t.DatasetController.prototype.removeHoverStyle.call(this,e,this.chart.options.elements.arc)},countVisibleElements:function(){var t=this.getDataset(),n=this.getMeta(),i=0;return e.each(n.data,function(e,n){isNaN(t.data[n])||e.hidden||i++}),i},calculateCircumference:function(t){var e=this.getMeta().count;return e>0&&!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={scale:{type:"radialLinear"},elements:{line:{tension:0}}},t.controllers.radar=t.DatasetController.extend({datasetElementType:t.elements.Line,dataElementType:t.elements.Point,linkScales:e.noop,addElementAndReset:function(e){t.DatasetController.prototype.addElementAndReset.call(this,e),this.updateBezierControlPoints()},update:function(t){var n=this,i=n.getMeta(),a=i.dataset,o=i.data,r=a.custom||{},s=n.getDataset(),l=n.chart.options.elements.line,d=n.chart.scale;void 0!==s.tension&&void 0===s.lineTension&&(s.lineTension=s.tension),e.extend(i.dataset,{_datasetIndex:n.index,_children:o,_loop:!0,_model:{tension:r.tension?r.tension:e.getValueOrDefault(s.lineTension,l.tension),backgroundColor:r.backgroundColor?r.backgroundColor:s.backgroundColor||l.backgroundColor,borderWidth:r.borderWidth?r.borderWidth:s.borderWidth||l.borderWidth,borderColor:r.borderColor?r.borderColor:s.borderColor||l.borderColor,fill:r.fill?r.fill:void 0!==s.fill?s.fill:l.fill,borderCapStyle:r.borderCapStyle?r.borderCapStyle:s.borderCapStyle||l.borderCapStyle,borderDash:r.borderDash?r.borderDash:s.borderDash||l.borderDash,borderDashOffset:r.borderDashOffset?r.borderDashOffset:s.borderDashOffset||l.borderDashOffset,borderJoinStyle:r.borderJoinStyle?r.borderJoinStyle:s.borderJoinStyle||l.borderJoinStyle,scaleTop:d.top,scaleBottom:d.bottom,scaleZero:d.getBasePosition()}}),i.dataset.pivot(),e.each(o,function(e,i){n.updateElement(e,i,t)},n),n.updateBezierControlPoints()},updateElement:function(t,n,i){var a=this,o=t.custom||{},r=a.getDataset(),s=a.chart.scale,l=a.chart.options.elements.point,d=s.getPointPositionForValue(n,r.data[n]);e.extend(t,{_datasetIndex:a.index,_index:n,_scale:s,_model:{x:i?s.xCenter:d.x,y:i?s.yCenter:d.y,tension:o.tension?o.tension:e.getValueOrDefault(r.tension,a.chart.options.elements.line.tension),radius:o.radius?o.radius:e.getValueAtIndexOrDefault(r.pointRadius,n,l.radius),backgroundColor:o.backgroundColor?o.backgroundColor:e.getValueAtIndexOrDefault(r.pointBackgroundColor,n,l.backgroundColor),borderColor:o.borderColor?o.borderColor:e.getValueAtIndexOrDefault(r.pointBorderColor,n,l.borderColor),borderWidth:o.borderWidth?o.borderWidth:e.getValueAtIndexOrDefault(r.pointBorderWidth,n,l.borderWidth),pointStyle:o.pointStyle?o.pointStyle:e.getValueAtIndexOrDefault(r.pointStyle,n,l.pointStyle),hitRadius:o.hitRadius?o.hitRadius:e.getValueAtIndexOrDefault(r.hitRadius,n,l.hitRadius)}}),t._model.skip=o.skip?o.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 o=i._model,r=e.splineCurve(e.previousItem(n.data,a,!0)._model,o,e.nextItem(n.data,a,!0)._model,o.tension);o.controlPointPreviousX=Math.max(Math.min(r.previous.x,t.right),t.left),o.controlPointPreviousY=Math.max(Math.min(r.previous.y,t.bottom),t.top),o.controlPointNextX=Math.max(Math.min(r.next.x,t.right),t.left),o.controlPointNextY=Math.max(Math.min(r.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,o=t._model;o.radius=i.hoverRadius?i.hoverRadius:e.getValueAtIndexOrDefault(n.pointHoverRadius,a,this.chart.options.elements.point.hoverRadius),o.backgroundColor=i.hoverBackgroundColor?i.hoverBackgroundColor:e.getValueAtIndexOrDefault(n.pointHoverBackgroundColor,a,e.getHoverColor(o.backgroundColor)),o.borderColor=i.hoverBorderColor?i.hoverBorderColor:e.getValueAtIndexOrDefault(n.pointHoverBorderColor,a,e.getHoverColor(o.borderColor)),o.borderWidth=i.hoverBorderWidth?i.hoverBorderWidth:e.getValueAtIndexOrDefault(n.pointHoverBorderWidth,a,o.borderWidth)},removeHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t.custom||{},a=t._index,o=t._model,r=this.chart.options.elements.point;o.radius=i.radius?i.radius:e.getValueAtIndexOrDefault(n.radius,a,r.radius),o.backgroundColor=i.backgroundColor?i.backgroundColor:e.getValueAtIndexOrDefault(n.pointBackgroundColor,a,r.backgroundColor),o.borderColor=i.borderColor?i.borderColor:e.getValueAtIndexOrDefault(n.pointBorderColor,a,r.borderColor),o.borderWidth=i.borderWidth?i.borderWidth:e.getValueAtIndexOrDefault(n.pointBorderWidth,a,r.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 o=0;o1&&(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(),o=(a-e)/t.frameDuration;t.dropFrames+=o,t.animations.length>0&&t.requestAnimationFrame()}}}},{}],22:[function(t,e,n){"use strict";e.exports=function(t){var e=t.canvasHelpers={};e.drawPoint=function(t,e,n,i,a){var o,r,s,l,d,u;if("object"==typeof e&&(o=e.toString(),"[object HTMLImageElement]"===o||"[object HTMLCanvasElement]"===o))return void t.drawImage(e,i-e.width/2,a-e.height/2);if(!(isNaN(n)||0>=n)){switch(e){default:t.beginPath(),t.arc(i,a,n,0,2*Math.PI),t.closePath(),t.fill();break;case"triangle":t.beginPath(),r=3*n/Math.sqrt(3),d=r*Math.sqrt(3)/2,t.moveTo(i-r/2,a+d/3),t.lineTo(i+r/2,a+d/3),t.lineTo(i,a-2*d/3),t.closePath(),t.fill();break;case"rect":u=1/Math.SQRT2*n,t.beginPath(),t.fillRect(i-u,a-u,2*u,2*u),t.strokeRect(i-u,a-u,2*u,2*u);break;case"rectRot":u=1/Math.SQRT2*n,t.beginPath(),t.moveTo(i-u,a),t.lineTo(i,a+u),t.lineTo(i+u,a),t.lineTo(i,a-u),t.closePath(),t.fill();break;case"cross":t.beginPath(),t.moveTo(i,a+n),t.lineTo(i,a-n),t.moveTo(i-n,a),t.lineTo(i+n,a),t.closePath();break;case"crossRot":t.beginPath(),s=Math.cos(Math.PI/4)*n,l=Math.sin(Math.PI/4)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l),t.moveTo(i-s,a+l),t.lineTo(i+s,a-l),t.closePath();break;case"star":t.beginPath(),t.moveTo(i,a+n),t.lineTo(i,a-n),t.moveTo(i-n,a),t.lineTo(i+n,a),s=Math.cos(Math.PI/4)*n,l=Math.sin(Math.PI/4)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l),t.moveTo(i-s,a+l),t.lineTo(i+s,a-l),t.closePath();break;case"line":t.beginPath(),t.moveTo(i-n,a),t.lineTo(i+n,a),t.closePath();break;case"dash":t.beginPath(),t.moveTo(i,a),t.lineTo(i+n,a),t.closePath()}t.stroke()}}}},{}],23:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.types={},t.instances={},t.controllers={},t.Controller=function(n){return this.chart=n,this.config=n.config,this.options=this.config.options=e.configMerge(t.defaults.global,t.defaults[this.config.type],this.config.options||{}),this.id=e.uid(),Object.defineProperty(this,"data",{get:function(){return this.config.data}}),t.instances[this.id]=this,this.options.responsive&&this.resize(!0),this.initialize(),this},e.extend(t.Controller.prototype,{initialize:function(){var e=this;return t.plugins.notify("beforeInit",[e]),e.bindEvents(),e.ensureScalesHaveIDs(),e.buildOrUpdateControllers(),e.buildScales(),e.updateLayout(),e.resetElements(),e.initToolTip(),e.update(),t.plugins.notify("afterInit",[e]),e},clear:function(){return e.clear(this.chart),this},stop:function(){return t.animationService.cancelAnimation(this),this},resize:function(n){var i=this,a=i.chart,o=a.canvas,r=e.getMaximumWidth(o),s=a.aspectRatio,l=i.options.maintainAspectRatio&&isNaN(s)===!1&&isFinite(s)&&0!==s?r/s:e.getMaximumHeight(o),d=a.width!==r||a.height!==l;if(!d)return i;o.width=a.width=r,o.height=a.height=l,e.retinaScale(a);var u={width:r,height:l};return t.plugins.notify("resize",[i,u]),i.options.onResize&&i.options.onResize(i,u),n||(i.stop(),i.update(i.options.responsiveAnimationDuration)),i},ensureScalesHaveIDs:function(){var t=this.options,n=t.scales||{},i=t.scale;e.each(n.xAxes,function(t,e){t.id=t.id||"x-axis-"+e}),e.each(n.yAxes,function(t,e){t.id=t.id||"y-axis-"+e}),i&&(i.id=i.id||"scale")},buildScales:function(){var n=this,i=n.options,a=n.scales={},o=[];i.scales&&(o=o.concat((i.scales.xAxes||[]).map(function(t){return{options:t,dtype:"category"}}),(i.scales.yAxes||[]).map(function(t){return{options:t,dtype:"linear"}}))),i.scale&&o.push({options:i.scale,dtype:"radialLinear",isDefault:!0}),e.each(o,function(i){var o=i.options,r=e.getValueOrDefault(o.type,i.dtype),s=t.scaleService.getScaleConstructor(r);if(s){var l=new s({id:o.id,options:o,ctx:n.chart.ctx,chart:n});a[l.id]=l,i.isDefault&&(n.scale=l)}}),t.scaleService.addScalesToLayout(this)},updateLayout:function(){t.layoutService.update(this,this.chart.width,this.chart.height)},buildOrUpdateControllers:function(){var n=this,i=[],a=[];if(e.each(n.data.datasets,function(e,o){var r=n.getDatasetMeta(o);r.type||(r.type=e.type||n.config.type),i.push(r.type),r.controller?r.controller.updateIndex(o):(r.controller=new t.controllers[r.type](n,o),a.push(r.controller))},n),i.length>1)for(var o=1;oe;++e)i.getDatasetMeta(e).controller.update();t.plugins.notify("afterDatasetsUpdate",[i])}},render:function(n,i){var a=this;t.plugins.notify("beforeRender",[a]);var o=a.options.animation;if(o&&("undefined"!=typeof n&&0!==n||"undefined"==typeof n&&0!==o.duration)){var r=new t.Animation;r.numSteps=(n||o.duration)/16.66,r.easing=o.easing,r.render=function(t,n){var i=e.easingEffects[n.easing],a=n.currentStep/n.numSteps,o=i(a);t.draw(o,a,n.currentStep)},r.onAnimationProgress=o.onProgress,r.onAnimationComplete=o.onComplete,t.animationService.addAnimation(a,r,n,i)}else a.draw(),o&&o.onComplete&&o.onComplete.call&&o.onComplete.call(a);return a},draw:function(n){var i=this,a=n||1;i.clear(),t.plugins.notify("beforeDraw",[i,a]),e.each(i.boxes,function(t){t.draw(i.chartArea)},i),i.scale&&i.scale.draw(),t.plugins.notify("beforeDatasetsDraw",[i,a]),e.each(i.data.datasets,function(t,e){i.isDatasetVisible(e)&&i.getDatasetMeta(e).controller.draw(n)},i,!0),t.plugins.notify("afterDatasetsDraw",[i,a]),i.tooltip.transition(a).draw(),t.plugins.notify("afterDraw",[i,a])},getElementAtEvent:function(t){var n=this,i=e.getRelativePosition(t,n.chart),a=[];return e.each(n.data.datasets,function(t,o){if(n.isDatasetVisible(o)){var r=n.getDatasetMeta(o);e.each(r.data,function(t){return t.inRange(i.x,i.y)?(a.push(t),a):void 0})}}),a.slice(0,1)},getElementsAtEvent:function(t){var n=this,i=e.getRelativePosition(t,n.chart),a=[],o=function(){if(n.data.datasets)for(var t=0;t0&&(e=this.getDatasetMeta(e[0]._datasetIndex).data),e},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;n>e;++e)this.isDatasetVisible(e)&&t++;return t},isDatasetVisible:function(t){var e=this.getDatasetMeta(t);return"boolean"==typeof e.hidden?!e.hidden:!this.data.datasets[t].hidden},generateLegend:function(){return this.options.legendCallback(this)},destroy:function(){var n=this;n.stop(),n.clear(),e.unbindEvents(n,n.events),e.removeResizeListener(n.chart.canvas.parentNode);var i=n.chart.canvas;i.width=n.chart.width,i.height=n.chart.height,void 0!==n.chart.originalDevicePixelRatio&&n.chart.ctx.scale(1/n.chart.originalDevicePixelRatio,1/n.chart.originalDevicePixelRatio),i.style.width=n.chart.originalCanvasStyleWidth,i.style.height=n.chart.originalCanvasStyleHeight,t.plugins.notify("destroy",[n]),delete t.instances[n.id]},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)},initToolTip:function(){var e=this;e.tooltip=new t.Tooltip({_chart:e.chart,_chartInstance:e,_data:e.data,_options:e.options.tooltips},e)},bindEvents:function(){var t=this;e.bindEvents(t,t.options.events,function(e){t.eventHandler(e)})},updateHoverStyle:function(t,e,n){var i,a,o,r=n?"setHoverStyle":"removeHoverStyle";switch(e){case"single":t=[t[0]];break;case"label":case"dataset":case"x-axis":break;default:return}for(a=0,o=t.length;o>a;++a)i=t[a],i&&this.getDatasetMeta(i._datasetIndex).controller[r](i)},eventHandler:function(t){var n=this,i=n.tooltip,a=n.options||{},o=a.hover,r=a.tooltips;return n.lastActive=n.lastActive||[],n.lastTooltipActive=n.lastTooltipActive||[],"mouseout"===t.type?(n.active=[],n.tooltipActive=[]):(n.active=n.getElementsAtEventForMode(t,o.mode),n.tooltipActive=n.getElementsAtEventForMode(t,r.mode)),o.onHover&&o.onHover.call(n,n.active),n.legend&&n.legend.handleEvent&&n.legend.handleEvent(t),("mouseup"===t.type||"click"===t.type)&&a.onClick&&a.onClick.call(n,t,n.active),n.lastActive.length&&n.updateHoverStyle(n.lastActive,o.mode,!1),n.active.length&&o.mode&&n.updateHoverStyle(n.active,o.mode,!0),(r.enabled||r.custom)&&(i.initialize(),i._active=n.tooltipActive,i.update(!0)),i.pivot(),n.animating||e.arrayEquals(n.active,n.lastActive)&&e.arrayEquals(n.tooltipActive,n.lastTooltipActive)||(n.stop(),(r.enabled||r.custom)&&i.update(!0),n.render(o.animationDuration,!0)),n.lastActive=n.active,n.lastTooltipActive=n.tooltipActive,n}})}},{}],24:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=e.noop;t.DatasetController=function(t,e){this.initialize(t,e)},e.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)},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||[],o=i.data;for(t=0,e=a.length;e>t;++t)o[t]=o[t]||n.createMetaData(i,t);i.dataset=i.dataset||n.createMetaDataset()},addElementAndReset:function(t){var e=this,n=e.createMetaData(t);e.getMeta().data.splice(t,0,n),e.updateElement(n,t,!0)},buildOrUpdateElements:function(){var t=this.getMeta(),e=t.data,n=this.getDataset().data.length,i=e.length;if(i>n)e.splice(n,i-n);else if(n>i)for(var a=i;n>a;++a)this.addElementAndReset(a)},update:n,draw:function(t){var n=t||1;e.each(this.getMeta().data,function(t){t.transition(n).draw()})},removeHoverStyle:function(t,n){var i=this.chart.data.datasets[t._datasetIndex],a=t._index,o=t.custom||{},r=e.getValueAtIndexOrDefault,s=t._model;s.backgroundColor=o.backgroundColor?o.backgroundColor:r(i.backgroundColor,a,n.backgroundColor),s.borderColor=o.borderColor?o.borderColor:r(i.borderColor,a,n.borderColor),s.borderWidth=o.borderWidth?o.borderWidth:r(i.borderWidth,a,n.borderWidth)},setHoverStyle:function(t){var n=this.chart.data.datasets[t._datasetIndex],i=t._index,a=t.custom||{},o=e.getValueAtIndexOrDefault,r=e.getHoverColor,s=t._model;s.backgroundColor=a.hoverBackgroundColor?a.hoverBackgroundColor:o(n.hoverBackgroundColor,i,r(s.backgroundColor)),s.borderColor=a.hoverBorderColor?a.hoverBorderColor:o(n.hoverBorderColor,i,r(s.borderColor)),s.borderWidth=a.hoverBorderWidth?a.hoverBorderWidth:o(n.hoverBorderWidth,i,s.borderWidth)}}),t.DatasetController.extend=e.inherits}},{}],25:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.elements={},t.Element=function(t){e.extend(this,t),this.initialize.apply(this,arguments)},e.extend(t.Element.prototype,{initialize:function(){this.hidden=!1},pivot:function(){var t=this;return t._view||(t._view=e.clone(t._model)),t._start=e.clone(t._view),t},transition:function(t){var n=this;return n._view||(n._view=e.clone(n._model)),1===t?(n._view=n._model,n._start=null,n):(n._start||n.pivot(),e.each(n._model,function(i,a){if("_"===a[0]);else if(n._view.hasOwnProperty(a))if(i===n._view[a]);else if("string"==typeof i)try{var o=e.color(n._model[a]).mix(e.color(n._start[a]),t);n._view[a]=o.rgbString()}catch(r){n._view[a]=i}else if("number"==typeof i){var s=void 0!==n._start[a]&&isNaN(n._start[a])===!1?n._start[a]:0;n._view[a]=(n._model[a]-s)*t+s}else n._view[a]=i;else"number"!=typeof i||isNaN(n._view[a])?n._view[a]=i:n._view[a]=i*t},n),n)},tooltipPosition:function(){return{x:this._model.x,y:this._model.y}},hasValue:function(){return e.isNumber(this._model.x)&&e.isNumber(this._model.y)}}),t.Element.extend=e.inherits}},{}],26:[function(t,e,n){"use strict";var i=t(2);e.exports=function(t){function e(t,e,n){var i;return"string"==typeof t?(i=parseInt(t,10),-1!==t.indexOf("%")&&(i=i/100*e.parentNode[n])):i=t,i}function n(t){return void 0!==t&&null!==t&&"none"!==t}function a(t,i,a){var o=document.defaultView,r=t.parentNode,s=o.getComputedStyle(t)[i],l=o.getComputedStyle(r)[i],d=n(s),u=n(l),c=Number.POSITIVE_INFINITY;return d||u?Math.min(d?e(s,t,a):c,u?e(l,r,a):c):"none"}var o=t.helpers={};o.each=function(t,e,n,i){var a,r;if(o.isArray(t))if(r=t.length,i)for(a=r-1;a>=0;a--)e.call(n,t[a],a);else for(a=0;r>a;a++)e.call(n,t[a],a);else if("object"==typeof t){var s=Object.keys(t);for(r=s.length,a=0;r>a;a++)e.call(n,t[s[a]],s[a])}},o.clone=function(t){var e={};return o.each(t,function(t,n){o.isArray(t)?e[n]=t.slice(0):"object"==typeof t&&null!==t?e[n]=o.clone(t):e[n]=t}),e},o.extend=function(t){for(var e=function(e,n){t[n]=e},n=1,i=arguments.length;i>n;n++)o.each(arguments[n],e);return t},o.configMerge=function(e){var n=o.clone(e);return o.each(Array.prototype.slice.call(arguments,1),function(e){o.each(e,function(e,i){if("scales"===i)n[i]=o.scaleMerge(n.hasOwnProperty(i)?n[i]:{},e);else if("scale"===i)n[i]=o.configMerge(n.hasOwnProperty(i)?n[i]:{},t.scaleService.getScaleDefaults(e.type),e);else if(n.hasOwnProperty(i)&&o.isArray(n[i])&&o.isArray(e)){var a=n[i];o.each(e,function(t,e){e=i[n].length||!i[n][a].type?i[n].push(o.configMerge(s,e)):e.type&&e.type!==i[n][a].type?i[n][a]=o.configMerge(i[n][a],s,e):i[n][a]=o.configMerge(i[n][a],e)}):(i[n]=[],o.each(e,function(e){var a=o.getValueOrDefault(e.type,"xAxes"===n?"category":"linear");i[n].push(o.configMerge(t.scaleService.getScaleDefaults(a),e))})):i.hasOwnProperty(n)&&"object"==typeof i[n]&&null!==i[n]&&"object"==typeof e?i[n]=o.configMerge(i[n],e):i[n]=e}),i},o.getValueAtIndexOrDefault=function(t,e,n){return void 0===t||null===t?n:o.isArray(t)?en;++n)if(t[n]===e)return n;return-1},o.where=function(t,e){if(o.isArray(t)&&Array.prototype.filter)return t.filter(e);var n=[];return o.each(t,function(t){e(t)&&n.push(t)}),n},o.findIndex=Array.prototype.findIndex?function(t,e,n){return t.findIndex(e,n)}:function(t,e,n){n=void 0===n?t:n;for(var i=0,a=t.length;a>i;++i)if(e.call(n,t[i],i,t))return i;return-1},o.findNextWhere=function(t,e,n){(void 0===n||null===n)&&(n=-1);for(var i=n+1;i=0;i--){var a=t[i];if(e(a))return a}},o.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=o.inherits,t&&o.extend(n.prototype,t),n.__super__=e.prototype,n},o.noop=function(){},o.uid=function(){var t=0;return function(){return t++}}(),o.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},o.almostEquals=function(t,e,n){return Math.abs(t-e)0?1:-1},o.log10=Math.log10?function(t){return Math.log10(t)}:function(t){return Math.log(t)/Math.LN10},o.toRadians=function(t){return t*(Math.PI/180)},o.toDegrees=function(t){return t*(180/Math.PI)},o.getAngleFromPoint=function(t,e){var n=e.x-t.x,i=e.y-t.y,a=Math.sqrt(n*n+i*i),o=Math.atan2(i,n);return o<-.5*Math.PI&&(o+=2*Math.PI),{angle:o,distance:a}},o.aliasPixel=function(t){return t%2===0?0:.5},o.splineCurve=function(t,e,n,i){var a=t.skip?e:t,o=e,r=n.skip?e:n,s=Math.sqrt(Math.pow(o.x-a.x,2)+Math.pow(o.y-a.y,2)),l=Math.sqrt(Math.pow(r.x-o.x,2)+Math.pow(r.y-o.y,2)),d=s/(s+l),u=l/(s+l);d=isNaN(d)?0:d,u=isNaN(u)?0:u;var c=i*d,h=i*u;return{previous:{x:o.x-c*(r.x-a.x),y:o.y-c*(r.y-a.y)},next:{x:o.x+h*(r.x-a.x),y:o.y+h*(r.y-a.y)}}},o.EPSILON=Number.EPSILON||1e-14,o.splineCurveMonotone=function(t){var e,n,i,a,r=(t||[]).map(function(t){return{model:t._model,deltaK:0,mK:0}}),s=r.length;for(e=0;s>e;++e)i=r[e],i.model.skip||(n=e>0?r[e-1]:null,a=s-1>e?r[e+1]:null,a&&!a.model.skip&&(i.deltaK=(a.model.y-i.model.y)/(a.model.x-i.model.x)),!n||n.model.skip?i.mK=i.deltaK:!a||a.model.skip?i.mK=n.deltaK:this.sign(n.deltaK)!==this.sign(i.deltaK)?i.mK=0:i.mK=(n.deltaK+i.deltaK)/2);var l,d,u,c;for(e=0;s-1>e;++e)i=r[e],a=r[e+1],i.model.skip||a.model.skip||(o.almostEquals(i.deltaK,0,this.EPSILON)?i.mK=a.mK=0:(l=i.mK/i.deltaK,d=a.mK/i.deltaK,c=Math.pow(l,2)+Math.pow(d,2),9>=c||(u=3/Math.sqrt(c),i.mK=l*u*i.deltaK,a.mK=d*u*i.deltaK)));var h;for(e=0;s>e;++e)i=r[e],i.model.skip||(n=e>0?r[e-1]:null,a=s-1>e?r[e+1]:null,n&&!n.model.skip&&(h=(i.model.x-n.model.x)/3,i.model.controlPointPreviousX=i.model.x-h,i.model.controlPointPreviousY=i.model.y-h*i.mK),a&&!a.model.skip&&(h=(a.model.x-i.model.x)/3,i.model.controlPointNextX=i.model.x+h,i.model.controlPointNextY=i.model.y+h*i.mK))},o.nextItem=function(t,e,n){return n?e>=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},o.previousItem=function(t,e,n){return n?0>=e?t[t.length-1]:t[e-1]:0>=e?t[0]:t[e-1]},o.niceNum=function(t,e){var n,i=Math.floor(o.log10(t)),a=t/Math.pow(10,i);return n=e?1.5>a?1:3>a?2:7>a?5:10:1>=a?1:2>=a?2:5>=a?5:10,n*Math.pow(10,i)};var r=o.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:-0.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:-0.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-0.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?-0.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),it?-.5*(i*Math.pow(2,10*(t-=1))*Math.sin((1*t-e)*(2*Math.PI)/n)):i*Math.pow(2,-10*(t-=1))*Math.sin((1*t-e)*(2*Math.PI)/n)*.5+1)},easeInBack:function(t){var e=1.70158;return 1*(t/=1)*t*((e+1)*t-e)},easeOutBack:function(t){var e=1.70158;return 1*((t=t/1-1)*t*((e+1)*t+e)+1)},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?.5*(t*t*(((e*=1.525)+1)*t-e)):.5*((t-=2)*t*(((e*=1.525)+1)*t+e)+2)},easeInBounce:function(t){return 1-r.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?1*(7.5625*t*t):2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*r.easeInBounce(2*t):.5*r.easeOutBounce(2*t-1)+.5}};o.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),o.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),o.getRelativePosition=function(t,e){var n,i,a=t.originalEvent||t,r=t.currentTarget||t.srcElement,s=r.getBoundingClientRect(),l=a.touches;l&&l.length>0?(n=l[0].clientX,i=l[0].clientY):(n=a.clientX,i=a.clientY);var d=parseFloat(o.getStyle(r,"padding-left")),u=parseFloat(o.getStyle(r,"padding-top")),c=parseFloat(o.getStyle(r,"padding-right")),h=parseFloat(o.getStyle(r,"padding-bottom")),f=s.right-s.left-d-c,g=s.bottom-s.top-u-h; +return n=Math.round((n-s.left-d)/f*r.width/e.currentDevicePixelRatio),i=Math.round((i-s.top-u)/g*r.height/e.currentDevicePixelRatio),{x:n,y:i}},o.addEvent=function(t,e,n){t.addEventListener?t.addEventListener(e,n):t.attachEvent?t.attachEvent("on"+e,n):t["on"+e]=n},o.removeEvent=function(t,e,n){t.removeEventListener?t.removeEventListener(e,n,!1):t.detachEvent?t.detachEvent("on"+e,n):t["on"+e]=o.noop},o.bindEvents=function(t,e,n){var i=t.events=t.events||{};o.each(e,function(e){i[e]=function(){n.apply(t,arguments)},o.addEvent(t.chart.canvas,e,i[e])})},o.unbindEvents=function(t,e){var n=t.chart.canvas;o.each(e,function(t,e){o.removeEvent(n,e,t)})},o.getConstraintWidth=function(t){return a(t,"max-width","clientWidth")},o.getConstraintHeight=function(t){return a(t,"max-height","clientHeight")},o.getMaximumWidth=function(t){var e=t.parentNode,n=parseInt(o.getStyle(e,"padding-left"),10),i=parseInt(o.getStyle(e,"padding-right"),10),a=e.clientWidth-n-i,r=o.getConstraintWidth(t);return isNaN(r)?a:Math.min(a,r)},o.getMaximumHeight=function(t){var e=t.parentNode,n=parseInt(o.getStyle(e,"padding-top"),10),i=parseInt(o.getStyle(e,"padding-bottom"),10),a=e.clientHeight-n-i,r=o.getConstraintHeight(t);return isNaN(r)?a:Math.min(a,r)},o.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},o.retinaScale=function(t){var e=t.ctx,n=t.canvas,i=n.width,a=n.height,o=t.currentDevicePixelRatio=window.devicePixelRatio||1;1!==o&&(n.height=a*o,n.width=i*o,e.scale(o,o),t.originalDevicePixelRatio=t.originalDevicePixelRatio||o),n.style.width=i+"px",n.style.height=a+"px"},o.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},o.fontString=function(t,e,n){return e+" "+t+"px "+n},o.longestText=function(t,e,n,i){i=i||{};var a=i.data=i.data||{},r=i.garbageCollect=i.garbageCollect||[];i.font!==e&&(a=i.data={},r=i.garbageCollect=[],i.font=e),t.font=e;var s=0;o.each(n,function(e){void 0!==e&&null!==e&&o.isArray(e)!==!0?s=o.measureText(t,a,r,s,e):o.isArray(e)&&o.each(e,function(e){void 0===e||null===e||o.isArray(e)||(s=o.measureText(t,a,r,s,e))})});var l=r.length/2;if(l>n.length){for(var d=0;l>d;d++)delete a[r[d]];r.splice(0,l)}return s},o.measureText=function(t,e,n,i,a){var o=e[a];return o||(o=e[a]=t.measureText(a).width,n.push(a)),o>i&&(i=o),i},o.numberOfLabelLines=function(t){var e=1;return o.each(t,function(t){o.isArray(t)&&t.length>e&&(e=t.length)}),e},o.drawRoundedRectangle=function(t,e,n,i,a,o){t.beginPath(),t.moveTo(e+o,n),t.lineTo(e+i-o,n),t.quadraticCurveTo(e+i,n,e+i,n+o),t.lineTo(e+i,n+a-o),t.quadraticCurveTo(e+i,n+a,e+i-o,n+a),t.lineTo(e+o,n+a),t.quadraticCurveTo(e,n+a,e,n+a-o),t.lineTo(e,n+o),t.quadraticCurveTo(e,n,e+o,n),t.closePath()},o.color=function(e){return i?i(e instanceof CanvasGradient?t.defaults.global.defaultColor:e):(console.error("Color.js not found!"),e)},o.addResizeListener=function(t,e){var n=document.createElement("iframe"),i="chartjs-hidden-iframe";n.classlist?n.classlist.add(i):n.setAttribute("class",i),n.tabIndex=-1;var a=n.style;a.width="100%",a.display="block",a.border=0,a.height=0,a.margin=0,a.position="absolute",a.left=0,a.right=0,a.top=0,a.bottom=0,t.insertBefore(n,t.firstChild),(n.contentWindow||n).onresize=function(){return e?e():void 0}},o.removeResizeListener=function(t){var e=t.querySelector(".chartjs-hidden-iframe");e&&e.parentNode.removeChild(e)},o.isArray=Array.isArray?function(t){return Array.isArray(t)}:function(t){return"[object Array]"===Object.prototype.toString.call(t)},o.arrayEquals=function(t,e){var n,i,a,r;if(!t||!e||t.length!==e.length)return!1;for(n=0,i=t.length;i>n;++n)if(a=t[n],r=e[n],a instanceof Array&&r instanceof Array){if(!o.arrayEquals(a,r))return!1}else if(a!==r)return!1;return!0},o.callCallback=function(t,e,n){t&&"function"==typeof t.call&&t.apply(n,e)},o.getHoverColor=function(t){return t instanceof CanvasPattern?t:o.color(t).saturate(.5).darken(.1).rgbString()}}},{2:2}],27:[function(t,e,n){"use strict";e.exports=function(){var t=function(e,n){var i=this,a=t.helpers;return i.config=n||{data:{datasets:[]}},e.length&&e[0].getContext&&(e=e[0]),e.getContext&&(e=e.getContext("2d")),i.ctx=e,i.canvas=e.canvas,e.canvas.style.display=e.canvas.style.display||"block",i.width=e.canvas.width||parseInt(a.getStyle(e.canvas,"width"),10)||a.getMaximumWidth(e.canvas),i.height=e.canvas.height||parseInt(a.getStyle(e.canvas,"height"),10)||a.getMaximumHeight(e.canvas),i.aspectRatio=i.width/i.height,(isNaN(i.aspectRatio)||isFinite(i.aspectRatio)===!1)&&(i.aspectRatio=void 0!==n.aspectRatio?n.aspectRatio:2),i.originalCanvasStyleWidth=e.canvas.style.width,i.originalCanvasStyleHeight=e.canvas.style.height,a.retinaScale(i),i.controller=new t.Controller(i),a.addResizeListener(e.canvas.parentNode,function(){i.controller&&i.controller.config.options.responsive&&i.controller.resize()}),i.controller?i.controller:i};return t.defaults={global:{responsive:!0,responsiveAnimationDuration:0,maintainAspectRatio:!0,events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"single",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}},{}],28:[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?m:k,x),S-=e.height):(e=t.update(y,b),k-=e.width),w.push({horizontal:n,minSize:e,box:t})}function o(t){var n=e.findNextWhere(w,function(e){return e.box===t});if(n)if(t.isHorizontal()){var i={left:_,right:M,top:0,bottom:0};t.update(t.options.fullWidth?m:k,p/2,i)}else t.update(n.minSize.width,S)}function r(t){var n=e.findNextWhere(w,function(e){return e.box===t}),i={left:0,right:0,top:D,bottom:C};n&&t.update(n.minSize.width,S,i)}function s(t){t.isHorizontal()?(t.left=t.options.fullWidth?l:_,t.right=t.options.fullWidth?n-l:_+k,t.top=I,t.bottom=I+t.height,I=t.bottom):(t.left=F,t.right=F+t.width,t.top=D,t.bottom=D+S,F=t.right)}if(t){var l=0,d=0,u=e.where(t.boxes,function(t){return"left"===t.options.position}),c=e.where(t.boxes,function(t){return"right"===t.options.position}),h=e.where(t.boxes,function(t){return"top"===t.options.position}),f=e.where(t.boxes,function(t){return"bottom"===t.options.position}),g=e.where(t.boxes,function(t){return"chartArea"===t.options.position});h.sort(function(t,e){return(e.options.fullWidth?1:0)-(t.options.fullWidth?1:0)}),f.sort(function(t,e){return(t.options.fullWidth?1:0)-(e.options.fullWidth?1:0)});var m=n-2*l,p=i-2*d,v=m/2,b=p/2,y=(n-v)/(u.length+c.length),x=(i-b)/(h.length+f.length),k=m,S=p,w=[];e.each(u.concat(c,h,f),a);var _=l,M=l,D=d,C=d;e.each(u.concat(c),o),e.each(u,function(t){_+=t.width}),e.each(c,function(t){M+=t.width}),e.each(h.concat(f),o),e.each(h,function(t){D+=t.height}),e.each(f,function(t){C+=t.height}),e.each(u.concat(c),r),_=l,M=l,D=d,C=d,e.each(u,function(t){_+=t.width}),e.each(c,function(t){M+=t.width}),e.each(h,function(t){D+=t.height}),e.each(f,function(t){C+=t.height});var T=i-D-C,P=n-_-M;(P!==k||T!==S)&&(e.each(u,function(t){t.height=T}),e.each(c,function(t){t.height=T}),e.each(h,function(t){t.options.fullWidth||(t.width=P)}),e.each(f,function(t){t.options.fullWidth||(t.width=P)}),S=T,k=P);var F=l,I=d;e.each(u.concat(h),s),F+=k,I+=S,e.each(c,s),e.each(f,s),t.chartArea={left:_,top:D,right:_+k,bottom:D+S},e.each(g,function(e){e.left=t.chartArea.left,e.top=t.chartArea.top,e.right=t.chartArea.right,e.bottom=t.chartArea.bottom,e.update(k,S)})}}}}},{}],29:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=e.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 n=t.data;return e.isArray(n.datasets)?n.datasets.map(function(n,i){return{text:n.label,fillStyle:e.isArray(n.backgroundColor)?n.backgroundColor[0]:n.backgroundColor,hidden:!t.isDatasetVisible(i),lineCap:n.borderCapStyle,lineDash:n.borderDash,lineDashOffset:n.borderDashOffset,lineJoin:n.borderJoinStyle,lineWidth:n.borderWidth,strokeStyle:n.borderColor,pointStyle:n.pointStyle,datasetIndex:i}},this):[]}}},t.Legend=t.Element.extend({initialize:function(t){e.extend(this,t),this.legendHitBoxes=[],this.doughnutMode=!1},beforeUpdate:n,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:n,beforeSetDimensions:n,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:n,beforeBuildLabels:n,buildLabels:function(){var t=this;t.legendItems=t.options.labels.generateLabels.call(t,t.chart),t.options.reverse&&t.legendItems.reverse()},afterBuildLabels:n,beforeFit:n,fit:function(){var n=this,i=n.options,a=i.labels,o=i.display,r=n.ctx,s=t.defaults.global,l=e.getValueOrDefault,d=l(a.fontSize,s.defaultFontSize),u=l(a.fontStyle,s.defaultFontStyle),c=l(a.fontFamily,s.defaultFontFamily),h=e.fontString(d,u,c),f=n.legendHitBoxes=[],g=n.minSize,m=n.isHorizontal();if(m?(g.width=n.maxWidth,g.height=o?10:0):(g.width=o?10:0,g.height=n.maxHeight),o)if(r.font=h,m){var p=n.lineWidths=[0],v=n.legendItems.length?d+a.padding:0;r.textAlign="left",r.textBaseline="top",e.each(n.legendItems,function(t,e){var i=a.usePointStyle?d*Math.sqrt(2):a.boxWidth,o=i+d/2+r.measureText(t.text).width;p[p.length-1]+o+a.padding>=n.width&&(v+=d+a.padding,p[p.length]=n.left),f[e]={left:0,top:0,width:o,height:d},p[p.length-1]+=o+a.padding}),g.height+=v}else{var b=a.padding,y=n.columnWidths=[],x=a.padding,k=0,S=0,w=d+b;e.each(n.legendItems,function(t,e){var n=a.usePointStyle?2*a.boxWidth:a.boxWidth,i=n+d/2+r.measureText(t.text).width;S+w>g.height&&(x+=k+a.padding,y.push(k),k=0,S=0),k=Math.max(k,i),S+=w,f[e]={left:0,top:0,width:i,height:d}}),x+=k,y.push(k),g.width+=x}n.width=g.width,n.height=g.height},afterFit:n,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var n=this,i=n.options,a=i.labels,o=t.defaults.global,r=o.elements.line,s=n.width,l=n.lineWidths;if(i.display){var d,u=n.ctx,c=e.getValueOrDefault,h=c(a.fontColor,o.defaultFontColor),f=c(a.fontSize,o.defaultFontSize),g=c(a.fontStyle,o.defaultFontStyle),m=c(a.fontFamily,o.defaultFontFamily),p=e.fontString(f,g,m);u.textAlign="left",u.textBaseline="top",u.lineWidth=.5,u.strokeStyle=h,u.fillStyle=h,u.font=p;var v=a.boxWidth,b=n.legendHitBoxes,y=function(e,n,a){if(!(isNaN(v)||0>=v)){u.save(),u.fillStyle=c(a.fillStyle,o.defaultColor),u.lineCap=c(a.lineCap,r.borderCapStyle),u.lineDashOffset=c(a.lineDashOffset,r.borderDashOffset),u.lineJoin=c(a.lineJoin,r.borderJoinStyle),u.lineWidth=c(a.lineWidth,r.borderWidth),u.strokeStyle=c(a.strokeStyle,o.defaultColor);var s=0===c(a.lineWidth,r.borderWidth);if(u.setLineDash&&u.setLineDash(c(a.lineDash,r.borderDash)),i.labels&&i.labels.usePointStyle){var l=f*Math.SQRT2/2,d=l/Math.SQRT2,h=e+d,g=n+d;t.canvasHelpers.drawPoint(u,a.pointStyle,l,h,g)}else s||u.strokeRect(e,n,v,f),u.fillRect(e,n,v,f);u.restore()}},x=function(t,e,n,i){u.fillText(n.text,v+f/2+t,e),n.hidden&&(u.beginPath(),u.lineWidth=2,u.moveTo(v+f/2+t,e+f/2),u.lineTo(v+f/2+t+i,e+f/2),u.stroke())},k=n.isHorizontal();d=k?{x:n.left+(s-l[0])/2,y:n.top+a.padding,line:0}:{x:n.left+a.padding,y:n.top+a.padding,line:0};var S=f+a.padding;e.each(n.legendItems,function(t,e){var i=u.measureText(t.text).width,o=a.usePointStyle?f+f/2+i:v+f/2+i,r=d.x,c=d.y;k?r+o>=s&&(c=d.y+=S,d.line++,r=d.x=n.left+(s-l[d.line])/2):c+S>n.bottom&&(r=d.x=r+n.columnWidths[d.line]+a.padding,c=d.y=n.top,d.line++),y(r,c,t),b[e].left=r,b[e].top=c,x(r,c,t,i),k?d.x+=o+a.padding:d.y+=S})}},handleEvent:function(t){var n=this,i=n.options,a="mouseup"===t.type?"click":t.type;if("mousemove"===a){if(!i.onHover)return}else{if("click"!==a)return;if(!i.onClick)return}var o=e.getRelativePosition(t,n.chart.chart),r=o.x,s=o.y;if(r>=n.left&&r<=n.right&&s>=n.top&&s<=n.bottom)for(var l=n.legendHitBoxes,d=0;d=u.left&&r<=u.left+u.width&&s>=u.top&&s<=u.top+u.height){if("click"===a){i.onClick.call(n,t,n.legendItems[d]);break}if("mousemove"===a){i.onHover.call(n,t,n.legendItems[d]);break}}}}}),t.plugins.register({beforeInit:function(e){var n=e.options,i=n.legend;i&&(e.legend=new t.Legend({ctx:e.chart.ctx,options:i,chart:e}),t.layoutService.addBox(e,e.legend))}})}},{}],30:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers.noop;t.plugins={_plugins:[],register:function(t){var e=this._plugins;[].concat(t).forEach(function(t){-1===e.indexOf(t)&&e.push(t)})},unregister:function(t){var e=this._plugins;[].concat(t).forEach(function(t){var n=e.indexOf(t);-1!==n&&e.splice(n,1)})},clear:function(){this._plugins=[]},count:function(){return this._plugins.length},getAll:function(){return this._plugins},notify:function(t,e){var n,i,a=this._plugins,o=a.length;for(n=0;o>n;++n)if(i=a[n],"function"==typeof i[t]&&i[t].apply(i,e||[])===!1)return!1;return!0}},t.PluginBase=t.Element.extend({beforeInit:e,afterInit:e,beforeUpdate:e,afterUpdate:e,beforeDraw:e,afterDraw:e,destroy:e}),t.pluginService=t.plugins}},{}],31:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers;t.defaults.scale={display:!0,position:"left",gridLines:{display:!0,color:"rgba(0, 0, 0, 0.1)",lineWidth:1,drawBorder:!0,drawOnChartArea:!0,drawTicks:!0,tickMarkLength:10,zeroLineWidth:1,zeroLineColor:"rgba(0,0,0,0.25)",offsetGridLines:!1,borderDash:[],borderDashOffset:0},scaleLabel:{labelString:"",display:!1},ticks:{beginAtZero:!1,minRotation:0,maxRotation:50,mirror:!1,padding:10,reverse:!1,display:!0,autoSkip:!0,autoSkipPadding:0,labelOffset:0,callback:function(t){return e.isArray(t)?t:""+t}}},t.Scale=t.Element.extend({beforeUpdate:function(){e.callCallback(this.options.beforeUpdate,[this])},update:function(t,n,i){var a=this;return a.beforeUpdate(),a.maxWidth=t,a.maxHeight=n,a.margins=e.extend({left:0,right:0,top:0,bottom:0},i),a.beforeSetDimensions(),a.setDimensions(),a.afterSetDimensions(),a.beforeDataLimits(),a.determineDataLimits(),a.afterDataLimits(),a.beforeBuildTicks(),a.buildTicks(),a.afterBuildTicks(),a.beforeTickToLabelConversion(),a.convertTicksToLabels(),a.afterTickToLabelConversion(),a.beforeCalculateTickRotation(),a.calculateTickRotation(),a.afterCalculateTickRotation(),a.beforeFit(),a.fit(),a.afterFit(),a.afterUpdate(),a.minSize},afterUpdate:function(){e.callCallback(this.options.afterUpdate,[this])},beforeSetDimensions:function(){e.callCallback(this.options.beforeSetDimensions,[this])},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},afterSetDimensions:function(){e.callCallback(this.options.afterSetDimensions,[this])},beforeDataLimits:function(){e.callCallback(this.options.beforeDataLimits,[this])},determineDataLimits:e.noop,afterDataLimits:function(){e.callCallback(this.options.afterDataLimits,[this])},beforeBuildTicks:function(){e.callCallback(this.options.beforeBuildTicks,[this])},buildTicks:e.noop,afterBuildTicks:function(){e.callCallback(this.options.afterBuildTicks,[this])},beforeTickToLabelConversion:function(){e.callCallback(this.options.beforeTickToLabelConversion,[this])},convertTicksToLabels:function(){var t=this;t.ticks=t.ticks.map(function(e,n,i){return t.options.ticks.userCallback?t.options.ticks.userCallback(e,n,i):t.options.ticks.callback(e,n,i)},t)},afterTickToLabelConversion:function(){e.callCallback(this.options.afterTickToLabelConversion,[this])},beforeCalculateTickRotation:function(){e.callCallback(this.options.beforeCalculateTickRotation,[this])},calculateTickRotation:function(){var n=this,i=n.ctx,a=t.defaults.global,o=n.options.ticks,r=e.getValueOrDefault(o.fontSize,a.defaultFontSize),s=e.getValueOrDefault(o.fontStyle,a.defaultFontStyle),l=e.getValueOrDefault(o.fontFamily,a.defaultFontFamily),d=e.fontString(r,s,l);i.font=d;var u,c=i.measureText(n.ticks[0]).width,h=i.measureText(n.ticks[n.ticks.length-1]).width;if(n.labelRotation=o.minRotation||0,n.paddingRight=0,n.paddingLeft=0,n.options.display&&n.isHorizontal()){n.paddingRight=h/2+3,n.paddingLeft=c/2+3,n.longestTextCache||(n.longestTextCache={});for(var f,g,m=e.longestText(i,d,n.ticks,n.longestTextCache),p=m,v=n.getPixelForTick(1)-n.getPixelForTick(0)-6;p>v&&n.labelRotationn.yLabelWidth&&(n.paddingLeft=u+r/2),n.paddingRight=r/2,g*m>n.maxHeight){n.labelRotation--;break}n.labelRotation++,p=f*m}}n.margins&&(n.paddingLeft=Math.max(n.paddingLeft-n.margins.left,0),n.paddingRight=Math.max(n.paddingRight-n.margins.right,0))},afterCalculateTickRotation:function(){e.callCallback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){e.callCallback(this.options.beforeFit,[this])},fit:function(){var n=this,i=n.minSize={width:0,height:0},a=n.options,o=t.defaults.global,r=a.ticks,s=a.scaleLabel,l=a.gridLines,d=a.display,u=n.isHorizontal(),c=e.getValueOrDefault(r.fontSize,o.defaultFontSize),h=e.getValueOrDefault(r.fontStyle,o.defaultFontStyle),f=e.getValueOrDefault(r.fontFamily,o.defaultFontFamily),g=e.fontString(c,h,f),m=e.getValueOrDefault(s.fontSize,o.defaultFontSize),p=a.gridLines.tickMarkLength;if(u?i.width=n.isFullWidth()?n.maxWidth-n.margins.left-n.margins.right:n.maxWidth:i.width=d&&l.drawTicks?p:0,u?i.height=d&&l.drawTicks?p:0:i.height=n.maxHeight,s.display&&d&&(u?i.height+=1.5*m:i.width+=1.5*m),r.display&&d){n.longestTextCache||(n.longestTextCache={});var v=e.longestText(n.ctx,g,n.ticks,n.longestTextCache),b=e.numberOfLabelLines(n.ticks),y=.5*c;if(u){n.longestLabelWidth=v;var x=Math.sin(e.toRadians(n.labelRotation))*n.longestLabelWidth+c*b+y*b;i.height=Math.min(n.maxHeight,i.height+x),n.ctx.font=g;var k=n.ctx.measureText(n.ticks[0]).width,S=n.ctx.measureText(n.ticks[n.ticks.length-1]).width,w=Math.cos(e.toRadians(n.labelRotation)),_=Math.sin(e.toRadians(n.labelRotation));n.paddingLeft=0!==n.labelRotation?w*k+3:k/2+3,n.paddingRight=0!==n.labelRotation?_*(c/2)+3:S/2+3}else{var M=n.maxWidth-i.width,D=r.mirror;D?v=0:v+=n.options.ticks.padding,M>v?i.width+=v:i.width=n.maxWidth,n.paddingTop=c/2,n.paddingBottom=c/2}}n.margins&&(n.paddingLeft=Math.max(n.paddingLeft-n.margins.left,0),n.paddingTop=Math.max(n.paddingTop-n.margins.top,0),n.paddingRight=Math.max(n.paddingRight-n.margins.right,0),n.paddingBottom=Math.max(n.paddingBottom-n.margins.bottom,0)),n.width=i.width,n.height=i.height},afterFit:function(){e.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&&isNaN(t)?NaN:"object"==typeof t?t instanceof Date||t.isValid?t:this.getRightValue(this.isHorizontal()?t.x:t.y):t},getLabelForIndex:e.noop,getPixelForValue:e.noop,getValueForPixel:e.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),o=a*t+n.paddingLeft;e&&(o+=a/2);var r=n.left+Math.round(o);return r+=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(){var t=this,e=t.min,n=t.max;return t.getPixelForValue(t.beginAtZero?0:0>e&&0>n?n:e>0&&n>0?e:0)},draw:function(n){var i=this,a=i.options;if(a.display){var o,r,s=i.ctx,l=t.defaults.global,d=a.ticks,u=a.gridLines,c=a.scaleLabel,h=0!==i.labelRotation,f=d.autoSkip,g=i.isHorizontal();d.maxTicksLimit&&(r=d.maxTicksLimit);var m=e.getValueOrDefault(d.fontColor,l.defaultFontColor),p=e.getValueOrDefault(d.fontSize,l.defaultFontSize),v=e.getValueOrDefault(d.fontStyle,l.defaultFontStyle),b=e.getValueOrDefault(d.fontFamily,l.defaultFontFamily),y=e.fontString(p,v,b),x=u.tickMarkLength,k=e.getValueOrDefault(u.borderDash,l.borderDash),S=e.getValueOrDefault(u.borderDashOffset,l.borderDashOffset),w=e.getValueOrDefault(c.fontColor,l.defaultFontColor),_=e.getValueOrDefault(c.fontSize,l.defaultFontSize),M=e.getValueOrDefault(c.fontStyle,l.defaultFontStyle),D=e.getValueOrDefault(c.fontFamily,l.defaultFontFamily),C=e.fontString(_,M,D),T=e.toRadians(i.labelRotation),P=Math.cos(T),F=i.longestLabelWidth*P;s.fillStyle=m;var I=[];if(g){if(o=!1,h&&(F/=2),(F+d.autoSkipPadding)*i.ticks.length>i.width-(i.paddingLeft+i.paddingRight)&&(o=1+Math.floor((F+d.autoSkipPadding)*i.ticks.length/(i.width-(i.paddingLeft+i.paddingRight)))),r&&i.ticks.length>r)for(;!o||i.ticks.length/(o||1)>r;)o||(o=1),o+=1;f||(o=!1)}var A="right"===a.position?i.left:i.right-x,O="right"===a.position?i.left+x:i.right,R="bottom"===a.position?i.top:i.bottom-x,W="bottom"===a.position?i.top+x:i.bottom;if(e.each(i.ticks,function(t,r){if(void 0!==t&&null!==t){var s=i.ticks.length===r+1,l=o>1&&r%o>0||r%o===0&&r+o>=i.ticks.length;if((!l||s)&&void 0!==t&&null!==t){var c,f;r===("undefined"!=typeof i.zeroLineIndex?i.zeroLineIndex:0)?(c=u.zeroLineWidth,f=u.zeroLineColor):(c=e.getValueAtIndexOrDefault(u.lineWidth,r),f=e.getValueAtIndexOrDefault(u.color,r));var m,p,v,b,y,w,_,M,D,C,P="middle",F="middle";if(g){h||(F="top"===a.position?"bottom":"top"),P=h?"right":"center";var L=i.getPixelForTick(r)+e.aliasPixel(c);D=i.getPixelForTick(r,u.offsetGridLines)+d.labelOffset,C=h?i.top+12:"top"===a.position?i.bottom-x:i.top+x,m=v=y=_=L,p=R,b=W,w=n.top,M=n.bottom}else{"left"===a.position?d.mirror?(D=i.right+d.padding,P="left"):(D=i.right-d.padding,P="right"):d.mirror?(D=i.left-d.padding,P="right"):(D=i.left+d.padding,P="left");var V=i.getPixelForTick(r);V+=e.aliasPixel(c),C=i.getPixelForTick(r,u.offsetGridLines),m=A,v=O,y=n.left,_=n.right,p=b=w=M=V}I.push({tx1:m,ty1:p,tx2:v,ty2:b,x1:y,y1:w,x2:_,y2:M,labelX:D,labelY:C,glWidth:c,glColor:f,glBorderDash:k,glBorderDashOffset:S,rotation:-1*T,label:t,textBaseline:F,textAlign:P})}}}),e.each(I,function(t){if(u.display&&(s.save(),s.lineWidth=t.glWidth,s.strokeStyle=t.glColor,s.setLineDash&&(s.setLineDash(t.glBorderDash),s.lineDashOffset=t.glBorderDashOffset),s.beginPath(),u.drawTicks&&(s.moveTo(t.tx1,t.ty1),s.lineTo(t.tx2,t.ty2)),u.drawOnChartArea&&(s.moveTo(t.x1,t.y1),s.lineTo(t.x2,t.y2)),s.stroke(),s.restore()),d.display){s.save(),s.translate(t.labelX,t.labelY),s.rotate(t.rotation),s.font=y,s.textBaseline=t.textBaseline,s.textAlign=t.textAlign;var n=t.label;if(e.isArray(n))for(var i=0,a=-(n.length-1)*p*.75;ie;++e){var o=t[e];if(o&&o.hasValue()){var r=o.tooltipPosition();i.push(r.x),a.push(r.y)}}var s=0,l=0;for(e=0;e0){var o=t[0];o.xLabel?n=o.xLabel:a>0&&o.indexe;++e)g.push(i(d[e]));s.itemSort&&(g=g.sort(function(t,e){return s.itemSort(t,e,u)})),d.length>1&&a.each(g,function(t){h.push(s.callbacks.labelColor.call(r,t,c))}),a.extend(l,{title:r.getTitle(g,u),beforeBody:r.getBeforeBody(g,u),body:r.getBody(g,u),afterBody:r.getAfterBody(g,u),footer:r.getFooter(g,u),x:Math.round(f.x),y:Math.round(f.y),caretPadding:a.getValueOrDefault(f.padding,2),labelColors:h});var m=r.getTooltipSize(l);r.determineAlignment(m),a.extend(l,r.getBackgroundPoint(l,m))}else r._model.opacity=0;return t&&s.custom&&s.custom.call(r,l),r},getTooltipSize:function(t){var e=this._chart.ctx,n={height:2*t.yPadding,width:0},i=t.body,o=i.reduce(function(t,e){ +return t+e.before.length+e.lines.length+e.after.length},0);o+=t.beforeBody.length+t.afterBody.length;var r=t.title.length,s=t.footer.length,l=t.titleFontSize,d=t.bodyFontSize,u=t.footerFontSize;n.height+=r*l,n.height+=(r-1)*t.titleSpacing,n.height+=r?t.titleMarginBottom:0,n.height+=o*d,n.height+=o?(o-1)*t.bodySpacing:0,n.height+=s?t.footerMarginTop:0,n.height+=s*u,n.height+=s?(s-1)*t.footerSpacing:0;var c=0,h=function(t){n.width=Math.max(n.width,e.measureText(t).width+c)};return e.font=a.fontString(l,t._titleFontStyle,t._titleFontFamily),a.each(t.title,h),e.font=a.fontString(d,t._bodyFontStyle,t._bodyFontFamily),a.each(t.beforeBody.concat(t.afterBody),h),c=i.length>1?d+2:0,a.each(i,function(t){a.each(t.before,h),a.each(t.lines,h),a.each(t.after,h)}),c=0,e.font=a.fontString(u,t._footerFontStyle,t._footerFontFamily),a.each(t.footer,h),n.width+=2*t.xPadding,n},determineAlignment:function(t){var e=this,n=e._model,i=e._chart,a=e._chartInstance.chartArea;n.yi.height-t.height&&(n.yAlign="bottom");var o,r,s,l,d,u=(a.left+a.right)/2,c=(a.top+a.bottom)/2;"center"===n.yAlign?(o=function(t){return u>=t},r=function(t){return t>u}):(o=function(e){return e<=t.width/2},r=function(e){return e>=i.width-t.width/2}),s=function(e){return e+t.width>i.width},l=function(e){return e-t.width<0},d=function(t){return c>=t?"top":"bottom"},o(n.x)?(n.xAlign="left",s(n.x)&&(n.xAlign="center",n.yAlign=d(n.y))):r(n.x)&&(n.xAlign="right",l(n.x)&&(n.xAlign="center",n.yAlign=d(n.y)))},getBackgroundPoint:function(t,e){var n={x:t.x,y:t.y},i=t.caretSize,a=t.caretPadding,o=t.cornerRadius,r=t.xAlign,s=t.yAlign,l=i+a,d=o+a;return"right"===r?n.x-=e.width:"center"===r&&(n.x-=e.width/2),"top"===s?n.y+=l:"bottom"===s?n.y-=e.height+l:n.y-=e.height/2,"center"===s?"left"===r?n.x+=l:"right"===r&&(n.x-=l):"left"===r?n.x-=d:"right"===r&&(n.x+=d),n},drawCaret:function(t,e,n){var i,o,r,s,l,d,u=this._view,c=this._chart.ctx,h=u.caretSize,f=u.cornerRadius,g=u.xAlign,m=u.yAlign,p=t.x,v=t.y,b=e.width,y=e.height;"center"===m?("left"===g?(i=p,o=i-h,r=i):(i=p+b,o=i+h,r=i),l=v+y/2,s=l-h,d=l+h):("left"===g?(i=p+f,o=i+h,r=o+h):"right"===g?(i=p+b-f,o=i-h,r=o-h):(o=p+b/2,i=o-h,r=o+h),"top"===m?(s=v,l=s-h,d=s):(s=v+y,l=s+h,d=s));var x=a.color(u.backgroundColor);c.fillStyle=x.alpha(n*x.alpha()).rgbString(),c.beginPath(),c.moveTo(i,s),c.lineTo(o,l),c.lineTo(r,d),c.closePath(),c.fill()},drawTitle:function(t,e,n,i){var o=e.title;if(o.length){n.textAlign=e._titleAlign,n.textBaseline="top";var r=e.titleFontSize,s=e.titleSpacing,l=a.color(e.titleFontColor);n.fillStyle=l.alpha(i*l.alpha()).rgbString(),n.font=a.fontString(r,e._titleFontStyle,e._titleFontFamily);var d,u;for(d=0,u=o.length;u>d;++d)n.fillText(o[d],t.x,t.y),t.y+=r+s,d+1===o.length&&(t.y+=e.titleMarginBottom-s)}},drawBody:function(t,e,n,i){var o=e.bodyFontSize,r=e.bodySpacing,s=e.body;n.textAlign=e._bodyAlign,n.textBaseline="top";var l=a.color(e.bodyFontColor),d=l.alpha(i*l.alpha()).rgbString();n.fillStyle=d,n.font=a.fontString(o,e._bodyFontStyle,e._bodyFontFamily);var u=0,c=function(e){n.fillText(e,t.x+u,t.y),t.y+=o+r};a.each(e.beforeBody,c);var h=s.length>1;u=h?o+2:0,a.each(s,function(r,s){a.each(r.before,c),a.each(r.lines,function(r){h&&(n.fillStyle=a.color(e.legendColorBackground).alpha(i).rgbaString(),n.fillRect(t.x,t.y,o,o),n.strokeStyle=a.color(e.labelColors[s].borderColor).alpha(i).rgbaString(),n.strokeRect(t.x,t.y,o,o),n.fillStyle=a.color(e.labelColors[s].backgroundColor).alpha(i).rgbaString(),n.fillRect(t.x+1,t.y+1,o-2,o-2),n.fillStyle=d),c(r)}),a.each(r.after,c)}),u=0,a.each(e.afterBody,c),t.y-=r},drawFooter:function(t,e,n,i){var o=e.footer;if(o.length){t.y+=e.footerMarginTop,n.textAlign=e._footerAlign,n.textBaseline="top";var r=a.color(e.footerFontColor);n.fillStyle=r.alpha(i*r.alpha()).rgbString(),n.font=a.fontString(e.footerFontSize,e._footerFontStyle,e._footerFontFamily),a.each(o,function(i){n.fillText(i,t.x,t.y),t.y+=e.footerFontSize+e.footerSpacing})}},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var n=this.getTooltipSize(e),i={x:e.x,y:e.y},o=Math.abs(e.opacity<.001)?0:e.opacity;if(this._options.enabled){var r=a.color(e.backgroundColor);t.fillStyle=r.alpha(o*r.alpha()).rgbString(),a.drawRoundedRectangle(t,i.x,i.y,n.width,n.height,e.cornerRadius),t.fill(),this.drawCaret(i,n,o),i.x+=e.xPadding,i.y+=e.yPadding,this.drawTitle(i,e,t,o),this.drawBody(i,e,t,o),this.drawFooter(i,e,t,o)}}}})}},{}],35:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n=t.defaults.global;n.elements.arc={backgroundColor:n.defaultColor,borderColor:"#fff",borderWidth:2},t.elements.Arc=t.Element.extend({inLabelRange:function(t){var e=this._view;return e?Math.pow(t-e.x,2)l;)l+=2*Math.PI;for(;o>l;)o-=2*Math.PI;for(;s>o;)o+=2*Math.PI;var d=o>=s&&l>=o,u=r>=i.innerRadius&&r<=i.outerRadius;return d&&u}return!1},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(){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,o=a.spanGaps,r=a.scaleZero,s=i._loop,l=i._chart.ctx;l.save();var d=i._children.slice(),u=-1;s&&d.length&&d.push(d[0]);var c,h,f,g;if(d.length&&a.fill){for(l.beginPath(),c=0;cc;c++)e.lineTo.apply(e,t(c));e.fill(),n.borderWidth&&e.stroke()},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){var n=this._view;return n?n.y=n.x-n.width/2&&t<=n.x+n.width/2&&e>=n.y&&e<=n.base:t>=n.x-n.width/2&&t<=n.x+n.width/2&&e>=n.base&&e<=n.y:!1},inLabelRange:function(t){var e=this._view;return e?t>=e.x-e.width/2&&t<=e.x+e.width/2:!1},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}})}},{}],39:[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=-1!==i?i:t.minIndex),void 0!==t.options.ticks.max&&(i=e.indexOf(n,t.options.ticks.max),t.maxIndex=-1!==i?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.xLabels&&a||i.yLabels&&!a?n.getRightValue(i.datasets[e].data[t]):n.ticks[t]},getPixelForValue:function(t,e,n,i){var a=this,o=Math.max(a.maxIndex+1-a.minIndex-(a.options.gridLines.offsetGridLines?0:1),1);if(void 0!==t&&isNaN(e)){var r=a.getLabels(),s=r.indexOf(t);e=-1!==s?s:e}if(a.isHorizontal()){var l=a.width-(a.paddingLeft+a.paddingRight),d=l/o,u=d*(e-a.minIndex)+a.paddingLeft;return(a.options.gridLines.offsetGridLines&&i||a.maxIndex===a.minIndex&&i)&&(u+=d/2),a.left+Math.round(u)}var c=a.height-(a.paddingTop+a.paddingBottom),h=c/o,f=h*(e-a.minIndex)+a.paddingTop;return a.options.gridLines.offsetGridLines&&i&&(f+=h/2),a.top+Math.round(f)},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(),o=a?n.width-(n.paddingLeft+n.paddingRight):n.height-(n.paddingTop+n.paddingBottom),r=o/i;return t-=a?n.left:n.top,n.options.gridLines.offsetGridLines&&(t-=r/2),t-=a?n.paddingLeft:n.paddingTop,e=0>=t?0:Math.round(t/r)},getBasePixel:function(){return this.bottom}});t.scaleService.registerScaleType("category",i,n)}},{}],40:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n={position:"left",ticks:{callback:function(t,n,i){var a=i.length>3?i[2]-i[1]:i[1]-i[0];Math.abs(a)>1&&t!==Math.floor(t)&&(a=t-Math.floor(t));var o=e.log10(Math.abs(a)),r="";if(0!==t){var s=-1*Math.floor(o);s=Math.max(Math.min(s,20),0),r=t.toFixed(s)}else r="0";return r}}},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,o=a.data,r=o.datasets,s=n.isHorizontal();if(n.min=null,n.max=null,i.stacked){var l={};e.each(r,function(o,r){var s=a.getDatasetMeta(r);void 0===l[s.type]&&(l[s.type]={positiveValues:[],negativeValues:[]});var d=l[s.type].positiveValues,u=l[s.type].negativeValues;a.isDatasetVisible(r)&&t(s)&&e.each(o.data,function(t,e){var a=+n.getRightValue(t);isNaN(a)||s.data[e].hidden||(d[e]=d[e]||0,u[e]=u[e]||0,i.relativePoints?d[e]=100:0>a?u[e]+=a:d[e]+=a)})}),e.each(l,function(t){var i=t.positiveValues.concat(t.negativeValues),a=e.min(i),o=e.max(i);n.min=null===n.min?a:Math.min(n.min,a),n.max=null===n.max?o:Math.max(n.max,o)})}else e.each(r,function(i,o){var r=a.getDatasetMeta(o);a.isDatasetVisible(o)&&t(r)&&e.each(i.data,function(t,e){var i=+n.getRightValue(t);isNaN(i)||r.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 o=e.getValueOrDefault(a.fontSize,t.defaults.global.defaultFontSize);n=Math.min(a.maxTicksLimit?a.maxTicksLimit:11,Math.ceil(i.height/(2*o)))}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,i=this,a=i.paddingLeft,o=i.paddingBottom,r=i.start,s=+i.getRightValue(t),l=i.end-r;return i.isHorizontal()?(n=i.width-(a+i.paddingRight),e=i.left+n/l*(s-r),Math.round(e+a)):(n=i.height-(i.paddingTop+o),e=i.bottom-o-n/l*(s-r),Math.round(e))},getValueForPixel:function(t){var e=this,n=e.isHorizontal(),i=e.paddingLeft,a=e.paddingBottom,o=n?e.width-(i+e.paddingRight):e.height-(e.paddingTop+a),r=(n?t-e.left-i:e.bottom-a-t)/o;return e.start+(e.end-e.start)*r},getPixelForTick:function(t){return this.getPixelForValue(this.ticksAsNumbers[t])}});t.scaleService.registerScaleType("linear",i,n)}},{}],41:[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),o=e.sign(t.max);0>a&&0>o?t.max=0:a>0&&o>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 t=this,n=t.options,i=t.ticks=[],a=n.ticks,o=e.getValueOrDefault,r=t.getTickLimit();r=Math.max(2,r);var s,l=a.fixedStepSize&&a.fixedStepSize>0||a.stepSize&&a.stepSize>0;if(l)s=o(a.fixedStepSize,a.stepSize);else{var d=e.niceNum(t.max-t.min,!1);s=e.niceNum(d/(r-1),!0)}var u=Math.floor(t.min/s)*s,c=Math.ceil(t.max/s)*s,h=(c-u)/s;h=e.almostEquals(h,Math.round(h),s/1e3)?Math.round(h):Math.ceil(h),i.push(void 0!==a.min?a.min:u);for(var f=1;h>f;++f)i.push(u+f*s);i.push(void 0!==a.max?a.max:c),t.handleDirectionalChanges(),t.max=e.max(i),t.min=e.min(i),a.reverse?(i.reverse(),t.start=t.max,t.end=t.min):(t.start=t.min,t.end=t.max)},convertTicksToLabels:function(){var e=this;e.ticksAsNumbers=e.ticks.slice(),e.zeroLineIndex=e.ticks.indexOf(0),t.Scale.prototype.convertTicksToLabels.call(e)}})}},{}],42:[function(t,e,n){"use strict";e.exports=function(t){var e=t.helpers,n={position:"left",ticks:{callback: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():""}}},i=t.Scale.extend({determineDataLimits:function(){function t(t){return d?t.xAxisID===n.id:t.yAxisID===n.id}var n=this,i=n.options,a=i.ticks,o=n.chart,r=o.data,s=r.datasets,l=e.getValueOrDefault,d=n.isHorizontal();if(n.min=null,n.max=null,n.minNotZero=null,i.stacked){var u={};e.each(s,function(a,r){var s=o.getDatasetMeta(r);o.isDatasetVisible(r)&&t(s)&&(void 0===u[s.type]&&(u[s.type]=[]),e.each(a.data,function(t,e){var a=u[s.type],o=+n.getRightValue(t);isNaN(o)||s.data[e].hidden||(a[e]=a[e]||0,i.relativePoints?a[e]=100:a[e]+=o)}))}),e.each(u,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 r=o.getDatasetMeta(a);o.isDatasetVisible(a)&&t(r)&&e.each(i.data,function(t,e){var i=+n.getRightValue(t);isNaN(i)||r.data[e].hidden||(null===n.min?n.min=i:in.max&&(n.max=i),0!==i&&(null===n.minNotZero||it.max&&(t.max=i))})}}),t.handleTickRangeOptions()},getTickLimit:function(){var t=this.options.ticks,i=e.getValueOrDefault(t.fontSize,n.defaultFontSize);return Math.min(t.maxTicksLimit?t.maxTicksLimit:11,Math.ceil(this.drawingArea/(1.5*i)))},convertTicksToLabels:function(){var e=this;t.LinearScaleBase.prototype.convertTicksToLabels.call(e),e.pointLabels=e.chart.data.labels.map(e.options.pointLabels.callback,e)},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var t,i,a,o,r,s,l,d,u,c,h,f,g=this.options.pointLabels,m=e.getValueOrDefault(g.fontSize,n.defaultFontSize),p=e.getValueOrDefault(g.fontStyle,n.defaultFontStyle),v=e.getValueOrDefault(g.fontFamily,n.defaultFontFamily),b=e.fontString(m,p,v),y=e.min([this.height/2-m-5,this.width/2]),x=this.width,k=0;for(this.ctx.font=b,i=0;ix&&(x=t.x+o,r=i),t.x-ow?t.x+a>x&&(x=t.x+a,r=i):t.x-ae&&0>n?n:e>0&&n>0?e:0)},draw:function(){var t=this,i=t.options,a=i.gridLines,o=i.ticks,r=i.angleLines,s=i.pointLabels,l=e.getValueOrDefault;if(i.display){var d=t.ctx,u=l(o.fontSize,n.defaultFontSize),c=l(o.fontStyle,n.defaultFontStyle),h=l(o.fontFamily,n.defaultFontFamily),f=e.fontString(u,c,h);if(e.each(t.ticks,function(r,s){if(s>0||i.reverse){var c=t.getDistanceFromCenterForValue(t.ticksAsNumbers[s]),h=t.yCenter-c;if(a.display&&0!==s)if(d.strokeStyle=e.getValueAtIndexOrDefault(a.color,s-1),d.lineWidth=e.getValueAtIndexOrDefault(a.lineWidth,s-1),i.lineArc)d.beginPath(),d.arc(t.xCenter,t.yCenter,c,0,2*Math.PI),d.closePath(),d.stroke();else{d.beginPath();for(var g=0;g=0;y--){if(r.display){var x=t.getPointPosition(y,g);d.beginPath(),d.moveTo(t.xCenter,t.yCenter),d.lineTo(x.x,x.y),d.stroke(),d.closePath()}var k=t.getPointPosition(y,g+5),S=l(s.fontColor,n.defaultFontColor);d.font=b,d.fillStyle=S;var w=t.pointLabels,_=this.getIndexAngle(y)+Math.PI/2,M=360*_/(2*Math.PI)%360;0===M||180===M?d.textAlign="center":180>M?d.textAlign="left":d.textAlign="right",90===M||270===M?d.textBaseline="middle":M>270||90>M?d.textBaseline="bottom":d.textBaseline="top",d.fillText(w[y]?w[y]:"",k.x,k.y)}}}}});t.scaleService.registerScaleType("radialLinear",a,i)}},{}],44:[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}},o=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,o){var r=[],s=t.chart.isDatasetVisible(o);"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),r.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):r=n,t.labelMoments.push(r)},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),o=e.getValueOrDefault(i.options.ticks.fontStyle,t.defaults.global.defaultFontStyle),r=e.getValueOrDefault(i.options.ticks.fontFamily,t.defaults.global.defaultFontFamily),s=e.fontString(a,o,r);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.paddingLeft+i.paddingRight):i.height-(i.paddingTop+i.paddingBottom),d=i.tickFormatFunction(i.firstTick,0,[]),u=i.ctx.measureText(d).width,c=Math.cos(e.toRadians(i.options.ticks.maxRotation)),h=Math.sin(e.toRadians(i.options.ticks.maxRotation));u=u*c+a*h;var f=l/u;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)k?i.lastTick=i.getMomentStartOf(i.lastTick.add(1,i.tickUnit)):k>=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 S=1;S<=i.scaleSizeInUnits;++S){var w=y.clone().add(S,i.tickUnit);if(i.options.time.max&&w.diff(i.lastTick,i.tickUnit,!0)>=0)break;S%i.unitScale===0&&i.ticks.push(w)}var _=i.ticks[i.ticks.length-1].diff(i.lastTick,i.tickUnit);(0!==_||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&&t').after(this.$container); + this.options.onInitialized(this.$select, this.$container); + } + + Multiselect.prototype = { + + defaults: { + /** + * Default text function will either print 'None selected' in case no + * option is selected or a list of the selected options up to a length + * of 3 selected options. + * + * @param {jQuery} options + * @param {jQuery} select + * @returns {String} + */ + buttonText: function(options, select) { + if (this.disabledText.length > 0 + && (select.prop('disabled') || (options.length == 0 && this.disableIfEmpty))) { + + return this.disabledText; + } + else if (options.length === 0) { + return this.nonSelectedText; + } + else if (this.allSelectedText + && options.length === $('option', $(select)).length + && $('option', $(select)).length !== 1 + && this.multiple) { + + if (this.selectAllNumber) { + return this.allSelectedText + ' (' + options.length + ')'; + } + else { + return this.allSelectedText; + } + } + else if (options.length > this.numberDisplayed) { + return options.length + ' ' + this.nSelectedText; + } + else { + var selected = ''; + var delimiter = this.delimiterText; + + options.each(function() { + var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); + selected += label + delimiter; + }); + + return selected.substr(0, selected.length - this.delimiterText.length); + } + }, + /** + * Updates the title of the button similar to the buttonText function. + * + * @param {jQuery} options + * @param {jQuery} select + * @returns {@exp;selected@call;substr} + */ + buttonTitle: function(options, select) { + if (options.length === 0) { + return this.nonSelectedText; + } + else { + var selected = ''; + var delimiter = this.delimiterText; + + options.each(function () { + var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); + selected += label + delimiter; + }); + return selected.substr(0, selected.length - this.delimiterText.length); + } + }, + checkboxName: function(option) { + return false; // no checkbox name + }, + /** + * Create a label. + * + * @param {jQuery} element + * @returns {String} + */ + optionLabel: function(element){ + return $(element).attr('label') || $(element).text(); + }, + /** + * Create a class. + * + * @param {jQuery} element + * @returns {String} + */ + optionClass: function(element) { + return $(element).attr('class') || ''; + }, + /** + * Triggered on change of the multiselect. + * + * Not triggered when selecting/deselecting options manually. + * + * @param {jQuery} option + * @param {Boolean} checked + */ + onChange : function(option, checked) { + + }, + /** + * Triggered when the dropdown is shown. + * + * @param {jQuery} event + */ + onDropdownShow: function(event) { + + }, + /** + * Triggered when the dropdown is hidden. + * + * @param {jQuery} event + */ + onDropdownHide: function(event) { + + }, + /** + * Triggered after the dropdown is shown. + * + * @param {jQuery} event + */ + onDropdownShown: function(event) { + + }, + /** + * Triggered after the dropdown is hidden. + * + * @param {jQuery} event + */ + onDropdownHidden: function(event) { + + }, + /** + * Triggered on select all. + */ + onSelectAll: function() { + + }, + /** + * Triggered on deselect all. + */ + onDeselectAll: function() { + + }, + /** + * Triggered after initializing. + * + * @param {jQuery} $select + * @param {jQuery} $container + */ + onInitialized: function($select, $container) { + + }, + /** + * Triggered on filtering. + * + * @param {jQuery} $filter + */ + onFiltering: function($filter) { + + }, + enableHTML: false, + buttonClass: 'btn btn-default', + inheritClass: false, + buttonWidth: 'auto', + buttonContainer: '
', + dropRight: false, + dropUp: false, + selectedClass: 'active', + // Maximum height of the dropdown menu. + // If maximum height is exceeded a scrollbar will be displayed. + maxHeight: false, + includeSelectAllOption: false, + includeSelectAllIfMoreThan: 0, + selectAllText: ' Select all', + selectAllValue: 'multiselect-all', + selectAllName: false, + selectAllNumber: true, + selectAllJustVisible: true, + enableFiltering: false, + enableCaseInsensitiveFiltering: false, + enableFullValueFiltering: false, + enableClickableOptGroups: false, + enableCollapsibleOptGroups: false, + filterPlaceholder: 'Search', + // possible options: 'text', 'value', 'both' + filterBehavior: 'text', + includeFilterClearBtn: true, + preventInputChangeEvent: false, + nonSelectedText: 'None selected', + nSelectedText: 'selected', + allSelectedText: 'All selected', + numberDisplayed: 3, + disableIfEmpty: false, + disabledText: '', + delimiterText: ', ', + templates: { + button: '', + ul: '', + filter: '
  • ', + filterClearBtn: '', + li: '
  • ', + divider: '
  • ', + liGroup: '
  • ' + } + }, + + constructor: Multiselect, + + /** + * Builds the container of the multiselect. + */ + buildContainer: function() { + this.$container = $(this.options.buttonContainer); + this.$container.on('show.bs.dropdown', this.options.onDropdownShow); + this.$container.on('hide.bs.dropdown', this.options.onDropdownHide); + this.$container.on('shown.bs.dropdown', this.options.onDropdownShown); + this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden); + }, + + /** + * Builds the button of the multiselect. + */ + buildButton: function() { + this.$button = $(this.options.templates.button).addClass(this.options.buttonClass); + if (this.$select.attr('class') && this.options.inheritClass) { + this.$button.addClass(this.$select.attr('class')); + } + // Adopt active state. + if (this.$select.prop('disabled')) { + this.disable(); + } + else { + this.enable(); + } + + // Manually add button width if set. + if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') { + this.$button.css({ + 'width' : '100%', //this.options.buttonWidth, + 'overflow' : 'hidden', + 'text-overflow' : 'ellipsis' + }); + this.$container.css({ + 'width': this.options.buttonWidth + }); + } + + // Keep the tab index from the select. + var tabindex = this.$select.attr('tabindex'); + if (tabindex) { + this.$button.attr('tabindex', tabindex); + } + + this.$container.prepend(this.$button); + }, + + /** + * Builds the ul representing the dropdown menu. + */ + buildDropdown: function() { + + // Build ul. + this.$ul = $(this.options.templates.ul); + + if (this.options.dropRight) { + this.$ul.addClass('pull-right'); + } + + // Set max height of dropdown menu to activate auto scrollbar. + if (this.options.maxHeight) { + // TODO: Add a class for this option to move the css declarations. + this.$ul.css({ + 'max-height': this.options.maxHeight + 'px', + 'overflow-y': 'auto', + 'overflow-x': 'hidden' + }); + } + + if (this.options.dropUp) { + + var height = Math.min(this.options.maxHeight, $('option[data-role!="divider"]', this.$select).length*26 + $('option[data-role="divider"]', this.$select).length*19 + (this.options.includeSelectAllOption ? 26 : 0) + (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering ? 44 : 0)); + var moveCalc = height + 34; + + this.$ul.css({ + 'max-height': height + 'px', + 'overflow-y': 'auto', + 'overflow-x': 'hidden', + 'margin-top': "-" + moveCalc + 'px' + }); + } + + this.$container.append(this.$ul); + }, + + /** + * Build the dropdown options and binds all necessary events. + * + * Uses createDivider and createOptionValue to create the necessary options. + */ + buildDropdownOptions: function() { + + this.$select.children().each($.proxy(function(index, element) { + + var $element = $(element); + // Support optgroups and options without a group simultaneously. + var tag = $element.prop('tagName') + .toLowerCase(); + + if ($element.prop('value') === this.options.selectAllValue) { + return; + } + + if (tag === 'optgroup') { + this.createOptgroup(element); + } + else if (tag === 'option') { + + if ($element.data('role') === 'divider') { + this.createDivider(); + } + else { + this.createOptionValue(element); + } + + } + + // Other illegal tags will be ignored. + }, this)); + + // Bind the change event on the dropdown elements. + $('li:not(.multiselect-group) input', this.$ul).on('change', $.proxy(function(event) { + var $target = $(event.target); + + var checked = $target.prop('checked') || false; + var isSelectAllOption = $target.val() === this.options.selectAllValue; + + // Apply or unapply the configured selected class. + if (this.options.selectedClass) { + if (checked) { + $target.closest('li') + .addClass(this.options.selectedClass); + } + else { + $target.closest('li') + .removeClass(this.options.selectedClass); + } + } + + // Get the corresponding option. + var value = $target.val(); + var $option = this.getOptionByValue(value); + + var $optionsNotThis = $('option', this.$select).not($option); + var $checkboxesNotThis = $('input', this.$container).not($target); + + if (isSelectAllOption) { + + if (checked) { + this.selectAll(this.options.selectAllJustVisible, true); + } + else { + this.deselectAll(this.options.selectAllJustVisible, true); + } + } + else { + if (checked) { + $option.prop('selected', true); + + if (this.options.multiple) { + // Simply select additional option. + $option.prop('selected', true); + } + else { + // Unselect all other options and corresponding checkboxes. + if (this.options.selectedClass) { + $($checkboxesNotThis).closest('li').removeClass(this.options.selectedClass); + } + + $($checkboxesNotThis).prop('checked', false); + $optionsNotThis.prop('selected', false); + + // It's a single selection, so close. + this.$button.click(); + } + + if (this.options.selectedClass === "active") { + $optionsNotThis.closest("a").css("outline", ""); + } + } + else { + // Unselect option. + $option.prop('selected', false); + } + + // To prevent select all from firing onChange: #575 + this.options.onChange($option, checked); + + // Do not update select all or optgroups on select all change! + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + } + + this.$select.change(); + this.updateButtonText(); + + if(this.options.preventInputChangeEvent) { + return false; + } + }, this)); + + $('li a', this.$ul).on('mousedown', function(e) { + if (e.shiftKey) { + // Prevent selecting text by Shift+click + return false; + } + }); + + $('li a', this.$ul).on('touchstart click', $.proxy(function(event) { + event.stopPropagation(); + + var $target = $(event.target); + + if (event.shiftKey && this.options.multiple) { + if($target.is("label")){ // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431) + event.preventDefault(); + $target = $target.find("input"); + $target.prop("checked", !$target.prop("checked")); + } + var checked = $target.prop('checked') || false; + + if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range + var from = $target.closest("li").index(); + var to = this.lastToggledInput.closest("li").index(); + + if (from > to) { // Swap the indices + var tmp = to; + to = from; + from = tmp; + } + + // Make sure we grab all elements since slice excludes the last index + ++to; + + // Change the checkboxes and underlying options + var range = this.$ul.find("li").slice(from, to).find("input"); + + range.prop('checked', checked); + + if (this.options.selectedClass) { + range.closest('li') + .toggleClass(this.options.selectedClass, checked); + } + + for (var i = 0, j = range.length; i < j; i++) { + var $checkbox = $(range[i]); + + var $option = this.getOptionByValue($checkbox.val()); + + $option.prop('selected', checked); + } + } + + // Trigger the select "change" event + $target.trigger("change"); + } + + // Remembers last clicked option + if($target.is("input") && !$target.closest("li").is(".multiselect-item")){ + this.lastToggledInput = $target; + } + + $target.blur(); + }, this)); + + // Keyboard support. + this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) { + if ($('input[type="text"]', this.$container).is(':focus')) { + return; + } + + if (event.keyCode === 9 && this.$container.hasClass('open')) { + this.$button.click(); + } + else { + var $items = $(this.$container).find("li:not(.divider):not(.disabled) a").filter(":visible"); + + if (!$items.length) { + return; + } + + var index = $items.index($items.filter(':focus')); + + // Navigation up. + if (event.keyCode === 38 && index > 0) { + index--; + } + // Navigate down. + else if (event.keyCode === 40 && index < $items.length - 1) { + index++; + } + else if (!~index) { + index = 0; + } + + var $current = $items.eq(index); + $current.focus(); + + if (event.keyCode === 32 || event.keyCode === 13) { + var $checkbox = $current.find('input'); + + $checkbox.prop("checked", !$checkbox.prop("checked")); + $checkbox.change(); + } + + event.stopPropagation(); + event.preventDefault(); + } + }, this)); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + $("li.multiselect-group input", this.$ul).on("change", $.proxy(function(event) { + event.stopPropagation(); + + var $target = $(event.target); + var checked = $target.prop('checked') || false; + + var $li = $(event.target).closest('li'); + var $group = $li.nextUntil("li.multiselect-group") + .not('.multiselect-filter-hidden') + .not('.disabled'); + + var $inputs = $group.find("input"); + + var values = []; + var $options = []; + + if (this.options.selectedClass) { + if (checked) { + $li.addClass(this.options.selectedClass); + } + else { + $li.removeClass(this.options.selectedClass); + } + } + + $.each($inputs, $.proxy(function(index, input) { + var value = $(input).val(); + var $option = this.getOptionByValue(value); + + if (checked) { + $(input).prop('checked', true); + $(input).closest('li') + .addClass(this.options.selectedClass); + + $option.prop('selected', true); + } + else { + $(input).prop('checked', false); + $(input).closest('li') + .removeClass(this.options.selectedClass); + + $option.prop('selected', false); + } + + $options.push(this.getOptionByValue(value)); + }, this)) + + // Cannot use select or deselect here because it would call updateOptGroups again. + + this.options.onChange($options, checked); + + this.updateButtonText(); + this.updateSelectAll(); + }, this)); + } + + if (this.options.enableCollapsibleOptGroups && this.options.multiple) { + $("li.multiselect-group .caret-container", this.$ul).on("click", $.proxy(function(event) { + var $li = $(event.target).closest('li'); + var $inputs = $li.nextUntil("li.multiselect-group") + .not('.multiselect-filter-hidden'); + + var visible = true; + $inputs.each(function() { + visible = visible && $(this).is(':visible'); + }); + + if (visible) { + $inputs.hide() + .addClass('multiselect-collapsible-hidden'); + } + else { + $inputs.show() + .removeClass('multiselect-collapsible-hidden'); + } + }, this)); + + $("li.multiselect-all", this.$ul).css('background', '#f3f3f3').css('border-bottom', '1px solid #eaeaea'); + $("li.multiselect-all > a > label.checkbox", this.$ul).css('padding', '3px 20px 3px 35px'); + $("li.multiselect-group > a > input", this.$ul).css('margin', '4px 0px 5px -20px'); + } + }, + + /** + * Create an option using the given select option. + * + * @param {jQuery} element + */ + createOptionValue: function(element) { + var $element = $(element); + if ($element.is(':selected')) { + $element.prop('selected', true); + } + + // Support the label attribute on options. + var label = this.options.optionLabel(element); + var classes = this.options.optionClass(element); + var value = $element.val(); + var inputType = this.options.multiple ? "checkbox" : "radio"; + + var $li = $(this.options.templates.li); + var $label = $('label', $li); + $label.addClass(inputType); + $li.addClass(classes); + + if (this.options.enableHTML) { + $label.html(" " + label); + } + else { + $label.text(" " + label); + } + + var $checkbox = $('').attr('type', inputType); + + var name = this.options.checkboxName($element); + if (name) { + $checkbox.attr('name', name); + } + + $label.prepend($checkbox); + + var selected = $element.prop('selected') || false; + $checkbox.val(value); + + if (value === this.options.selectAllValue) { + $li.addClass("multiselect-item multiselect-all"); + $checkbox.parent().parent() + .addClass('multiselect-all'); + } + + $label.attr('title', $element.attr('title')); + + this.$ul.append($li); + + if ($element.is(':disabled')) { + $checkbox.attr('disabled', 'disabled') + .prop('disabled', true) + .closest('a') + .attr("tabindex", "-1") + .closest('li') + .addClass('disabled'); + } + + $checkbox.prop('checked', selected); + + if (selected && this.options.selectedClass) { + $checkbox.closest('li') + .addClass(this.options.selectedClass); + } + }, + + /** + * Creates a divider using the given select option. + * + * @param {jQuery} element + */ + createDivider: function(element) { + var $divider = $(this.options.templates.divider); + this.$ul.append($divider); + }, + + /** + * Creates an optgroup. + * + * @param {jQuery} group + */ + createOptgroup: function(group) { + var label = $(group).attr("label"); + var value = $(group).attr("value"); + var $li = $('
  • '); + + var classes = this.options.optionClass(group); + $li.addClass(classes); + + if (this.options.enableHTML) { + $('label b', $li).html(" " + label); + } + else { + $('label b', $li).text(" " + label); + } + + if (this.options.enableCollapsibleOptGroups && this.options.multiple) { + $('a', $li).append(''); + } + + if (this.options.enableClickableOptGroups && this.options.multiple) { + $('a label', $li).prepend(''); + } + + if ($(group).is(':disabled')) { + $li.addClass('disabled'); + } + + this.$ul.append($li); + + $("option", group).each($.proxy(function($, group) { + this.createOptionValue(group); + }, this)) + }, + + /** + * Build the select all. + * + * Checks if a select all has already been created. + */ + buildSelectAll: function() { + if (typeof this.options.selectAllValue === 'number') { + this.options.selectAllValue = this.options.selectAllValue.toString(); + } + + var alreadyHasSelectAll = this.hasSelectAll(); + + if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple + && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) { + + // Check whether to add a divider after the select all. + if (this.options.includeSelectAllDivider) { + this.$ul.prepend($(this.options.templates.divider)); + } + + var $li = $(this.options.templates.li); + $('label', $li).addClass("checkbox"); + + if (this.options.enableHTML) { + $('label', $li).html(" " + this.options.selectAllText); + } + else { + $('label', $li).text(" " + this.options.selectAllText); + } + + if (this.options.selectAllName) { + $('label', $li).prepend(''); + } + else { + $('label', $li).prepend(''); + } + + var $checkbox = $('input', $li); + $checkbox.val(this.options.selectAllValue); + + $li.addClass("multiselect-item multiselect-all"); + $checkbox.parent().parent() + .addClass('multiselect-all'); + + this.$ul.prepend($li); + + $checkbox.prop('checked', false); + } + }, + + /** + * Builds the filter. + */ + buildFilter: function() { + + // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength. + if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) { + var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering); + + if (this.$select.find('option').length >= enableFilterLength) { + + this.$filter = $(this.options.templates.filter); + $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder); + + // Adds optional filter clear button + if(this.options.includeFilterClearBtn) { + var clearBtn = $(this.options.templates.filterClearBtn); + clearBtn.on('click', $.proxy(function(event){ + clearTimeout(this.searchTimeout); + + this.$filter.find('.multiselect-search').val(''); + $('li', this.$ul).show().removeClass('multiselect-filter-hidden'); + + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + }, this)); + this.$filter.find('.input-group').append(clearBtn); + } + + this.$ul.prepend(this.$filter); + + this.$filter.val(this.query).on('click', function(event) { + event.stopPropagation(); + }).on('input keydown', $.proxy(function(event) { + // Cancel enter key default behaviour + if (event.which === 13) { + event.preventDefault(); + } + + // This is useful to catch "keydown" events after the browser has updated the control. + clearTimeout(this.searchTimeout); + + this.searchTimeout = this.asyncFunction($.proxy(function() { + + if (this.query !== event.target.value) { + this.query = event.target.value; + + var currentGroup, currentGroupVisible; + $.each($('li', this.$ul), $.proxy(function(index, element) { + var value = $('input', element).length > 0 ? $('input', element).val() : ""; + var text = $('label', element).text(); + + var filterCandidate = ''; + if ((this.options.filterBehavior === 'text')) { + filterCandidate = text; + } + else if ((this.options.filterBehavior === 'value')) { + filterCandidate = value; + } + else if (this.options.filterBehavior === 'both') { + filterCandidate = text + '\n' + value; + } + + if (value !== this.options.selectAllValue && text) { + + // By default lets assume that element is not + // interesting for this search. + var showElement = false; + + if (this.options.enableCaseInsensitiveFiltering) { + filterCandidate = filterCandidate.toLowerCase(); + this.query = this.query.toLowerCase(); + } + + if (this.options.enableFullValueFiltering && this.options.filterBehavior !== 'both') { + var valueToMatch = filterCandidate.trim().substring(0, this.query.length); + if (this.query.indexOf(valueToMatch) > -1) { + showElement = true; + } + } + else if (filterCandidate.indexOf(this.query) > -1) { + showElement = true; + } + + // Toggle current element (group or group item) according to showElement boolean. + $(element).toggle(showElement) + .toggleClass('multiselect-filter-hidden', !showElement); + + // Differentiate groups and group items. + if ($(element).hasClass('multiselect-group')) { + // Remember group status. + currentGroup = element; + currentGroupVisible = showElement; + } + else { + // Show group name when at least one of its items is visible. + if (showElement) { + $(currentGroup).show() + .removeClass('multiselect-filter-hidden'); + } + + // Show all group items when group name satisfies filter. + if (!showElement && currentGroupVisible) { + $(element).show() + .removeClass('multiselect-filter-hidden'); + } + } + } + }, this)); + } + + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + this.options.onFiltering(event.target); + + }, this), 300, this); + }, this)); + } + } + }, + + /** + * Unbinds the whole plugin. + */ + destroy: function() { + this.$container.remove(); + this.$select.show(); + + // reset original state + this.$select.prop('disabled', this.options.wasDisabled); + + this.$select.data('multiselect', null); + }, + + /** + * Refreshs the multiselect based on the selected options of the select. + */ + refresh: function () { + var inputs = $.map($('li input', this.$ul), $); + + $('option', this.$select).each($.proxy(function (index, element) { + var $elem = $(element); + var value = $elem.val(); + var $input; + for (var i = inputs.length; 0 < i--; /**/) { + if (value !== ($input = inputs[i]).val()) + continue; // wrong li + + if ($elem.is(':selected')) { + $input.prop('checked', true); + + if (this.options.selectedClass) { + $input.closest('li') + .addClass(this.options.selectedClass); + } + } + else { + $input.prop('checked', false); + + if (this.options.selectedClass) { + $input.closest('li') + .removeClass(this.options.selectedClass); + } + } + + if ($elem.is(":disabled")) { + $input.attr('disabled', 'disabled') + .prop('disabled', true) + .closest('li') + .addClass('disabled'); + } + else { + $input.prop('disabled', false) + .closest('li') + .removeClass('disabled'); + } + break; // assumes unique values + } + }, this)); + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Select all options of the given values. + * + * If triggerOnChange is set to true, the on change event is triggered if + * and only if one value is passed. + * + * @param {Array} selectValues + * @param {Boolean} triggerOnChange + */ + select: function(selectValues, triggerOnChange) { + if(!$.isArray(selectValues)) { + selectValues = [selectValues]; + } + + for (var i = 0; i < selectValues.length; i++) { + var value = selectValues[i]; + + if (value === null || value === undefined) { + continue; + } + + var $option = this.getOptionByValue(value); + var $checkbox = this.getInputByValue(value); + + if($option === undefined || $checkbox === undefined) { + continue; + } + + if (!this.options.multiple) { + this.deselectAll(false); + } + + if (this.options.selectedClass) { + $checkbox.closest('li') + .addClass(this.options.selectedClass); + } + + $checkbox.prop('checked', true); + $option.prop('selected', true); + + if (triggerOnChange) { + this.options.onChange($option, true); + } + } + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Clears all selected items. + */ + clearSelection: function () { + this.deselectAll(false); + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Deselects all options of the given values. + * + * If triggerOnChange is set to true, the on change event is triggered, if + * and only if one value is passed. + * + * @param {Array} deselectValues + * @param {Boolean} triggerOnChange + */ + deselect: function(deselectValues, triggerOnChange) { + if(!$.isArray(deselectValues)) { + deselectValues = [deselectValues]; + } + + for (var i = 0; i < deselectValues.length; i++) { + var value = deselectValues[i]; + + if (value === null || value === undefined) { + continue; + } + + var $option = this.getOptionByValue(value); + var $checkbox = this.getInputByValue(value); + + if($option === undefined || $checkbox === undefined) { + continue; + } + + if (this.options.selectedClass) { + $checkbox.closest('li') + .removeClass(this.options.selectedClass); + } + + $checkbox.prop('checked', false); + $option.prop('selected', false); + + if (triggerOnChange) { + this.options.onChange($option, false); + } + } + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Selects all enabled & visible options. + * + * If justVisible is true or not specified, only visible options are selected. + * + * @param {Boolean} justVisible + * @param {Boolean} triggerOnSelectAll + */ + selectAll: function (justVisible, triggerOnSelectAll) { + + var justVisible = typeof justVisible === 'undefined' ? true : justVisible; + var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); + var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); + + if(justVisible) { + $('input:enabled' , visibleLis).prop('checked', true); + visibleLis.addClass(this.options.selectedClass); + + $('input:enabled' , visibleLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', true); + }, this)); + } + else { + $('input:enabled' , allLis).prop('checked', true); + allLis.addClass(this.options.selectedClass); + + $('input:enabled' , allLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', true); + }, this)); + } + + $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (triggerOnSelectAll) { + this.options.onSelectAll(); + } + }, + + /** + * Deselects all options. + * + * If justVisible is true or not specified, only visible options are deselected. + * + * @param {Boolean} justVisible + */ + deselectAll: function (justVisible, triggerOnDeselectAll) { + + var justVisible = typeof justVisible === 'undefined' ? true : justVisible; + var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); + var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); + + if(justVisible) { + $('input[type="checkbox"]:enabled' , visibleLis).prop('checked', false); + visibleLis.removeClass(this.options.selectedClass); + + $('input[type="checkbox"]:enabled' , visibleLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', false); + }, this)); + } + else { + $('input[type="checkbox"]:enabled' , allLis).prop('checked', false); + allLis.removeClass(this.options.selectedClass); + + $('input[type="checkbox"]:enabled' , allLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', false); + }, this)); + } + + $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', false); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (triggerOnDeselectAll) { + this.options.onDeselectAll(); + } + }, + + /** + * Rebuild the plugin. + * + * Rebuilds the dropdown, the filter and the select all option. + */ + rebuild: function() { + this.$ul.html(''); + + // Important to distinguish between radios and checkboxes. + this.options.multiple = this.$select.attr('multiple') === "multiple"; + + this.buildSelectAll(); + this.buildDropdownOptions(); + this.buildFilter(); + + this.updateButtonText(); + this.updateSelectAll(true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { + this.disable(); + } + else { + this.enable(); + } + + if (this.options.dropRight) { + this.$ul.addClass('pull-right'); + } + }, + + /** + * The provided data will be used to build the dropdown. + */ + dataprovider: function(dataprovider) { + + var groupCounter = 0; + var $select = this.$select.empty(); + + $.each(dataprovider, function (index, option) { + var $tag; + + if ($.isArray(option.children)) { // create optiongroup tag + groupCounter++; + + $tag = $('').attr({ + label: option.label || 'Group ' + groupCounter, + disabled: !!option.disabled + }); + + forEach(option.children, function(subOption) { // add children option tags + var attributes = { + value: subOption.value, + label: subOption.label || subOption.value, + title: subOption.title, + selected: !!subOption.selected, + disabled: !!subOption.disabled + }; + + //Loop through attributes object and add key-value for each attribute + for (var key in subOption.attributes) { + attributes['data-' + key] = subOption.attributes[key]; + } + //Append original attributes + new data attributes to option + $tag.append($('
    {{ ExpandedForm.text('name') }} + {% if account.accounttype.type == 'Default account' or account.accounttype.type == 'Asset account' %} + {# Not really mandatory but OK #} + {{ ExpandedForm.select('currency_id', currencies) }} + {% endif %}
    @@ -28,12 +32,13 @@
    {{ ExpandedForm.text('iban') }} + {{ ExpandedForm.text('BIC', null, {maxlength: 11}) }} {{ ExpandedForm.text('accountNumber') }} {% if account.accounttype.type == 'Default account' or account.accounttype.type == 'Asset account' %} {{ ExpandedForm.balance('openingBalance',null, {'currency' : openingBalance ? openingBalance.transactionCurrency : null}) }} {{ ExpandedForm.date('openingBalanceDate') }} - {{ ExpandedForm.select('accountRole',Config.get('firefly.accountRoles')) }} + {{ ExpandedForm.select('accountRole', roles) }} {{ ExpandedForm.balance('virtualBalance',null) }} {% endif %} @@ -72,3 +77,13 @@
    {{ Form.close|raw }} {% endblock %} +{% block scripts %} + + + +{% endblock %} + +{% block styles %} + + +{% endblock %} diff --git a/resources/views/accounts/index.twig b/resources/views/accounts/index.twig index af0a8d246d..84f653aacb 100644 --- a/resources/views/accounts/index.twig +++ b/resources/views/accounts/index.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, what) }} @@ -26,7 +26,7 @@
    - {% include 'list/accounts.twig' %} + {% include 'list.accounts' %}
    @@ -39,7 +39,7 @@ {% block scripts %} {% endblock %} diff --git a/resources/views/accounts/show.twig b/resources/views/accounts/show.twig index 61d41da0c5..4d9d3444a8 100644 --- a/resources/views/accounts/show.twig +++ b/resources/views/accounts/show.twig @@ -1,7 +1,7 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} - {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, account) }} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, account, start, end) }} {% endblock %} {% block content %} @@ -9,7 +9,8 @@
    -

    {{ account.name }}

    +

    {{ account.name }} + ({{ trans('firefly.from_to', {start: start.formatLocalized(monthAndDayFormat), end: end.formatLocalized(monthAndDayFormat)}) }})

    @@ -28,48 +29,110 @@
    - +
    +
    +
    +
    +

    {{ 'expenses_by_category'|_ }}

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    {{ 'expenses_by_budget'|_ }}

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    {{ 'income_by_category'|_ }}

    +
    +
    +
    + +
    +
    +
    +
    +
    +{% if entries %} + +{% endif %}
    -
    +

    {{ 'transactions'|_ }}

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

    + + + {{ 'show_all_no_filter'|_ }} + +

    + {% else %} +

    + + + {{ 'show_the_current_period_and_overview'|_ }} + +

    + {% endif %}
    -
    - {% for entry in entries %} - {% if entry[2] != 0 or entry[3] != 0 %} -
    - -
    - - {% if entry[2] != 0 %} - - - - - {% endif %} - {% if entry[3] != 0 %} - - - - - {% endif %} -
    {{ 'spent'|_ }}{{ entry[2]|formatAmount }}
    {{ 'earned'|_ }}{{ entry[3]|formatAmount }}
    -
    -
    - {% endif %} + {% if entries %} +
    - {% endfor %} -
    + {% for entry in entries %} + {% if (entry[2] != 0 or entry[3] != 0) or (accountType == 'Asset account') %} +
    + +
    + + {% if entry[2] != 0 or (accountType == 'Asset account') %} + + + + + {% endif %} + {% if entry[3] != 0 or (accountType == 'Asset account') %} + + + + + {% endif %} +
    {{ 'spent'|_ }}{{ entry[2]|formatAmount }}
    {{ 'earned'|_ }}{{ entry[3]|formatAmount }}
    +
    +
    + {% endif %} + {% endfor %} +

    {{ 'showEverything'|_ }}

    +
    + {% endif %}
    @@ -79,8 +142,16 @@ {% block scripts %} + + diff --git a/resources/views/accounts/show_with_date.twig b/resources/views/accounts/show_with_date.twig deleted file mode 100644 index 89ae367972..0000000000 --- a/resources/views/accounts/show_with_date.twig +++ /dev/null @@ -1,63 +0,0 @@ -{% extends "./layout/default.twig" %} - -{% block breadcrumbs %} - - {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, account, carbon) }} -{% endblock %} - -{% block content %} - -
    -
    -
    -
    -

    {{ 'overview'|_ }} (period)

    - - - - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    -

    {{ 'transactions'|_ }}

    -
    -
    - {% include 'list.journals-tasker' with {sorting:true} %} -
    -
    -
    -
    - - - -{% endblock %} - -{% block scripts %} - - - - - - - -{% endblock %} diff --git a/resources/views/admin/configuration/index.twig b/resources/views/admin/configuration/index.twig index 412fb2459b..a3a15e629b 100644 --- a/resources/views/admin/configuration/index.twig +++ b/resources/views/admin/configuration/index.twig @@ -1,16 +1,16 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists }} {% endblock %} {% block content %} -
    +
    - + {# single user mode #}
    @@ -25,21 +25,38 @@
    + {# installation is demo site #} +
    +
    +
    +

    {{ 'setting_is_demo_site'|_ }}

    +
    +
    +

    + {{ 'setting_is_demo_site_explain'|_ }} +

    + {{ ExpandedForm.checkbox('is_demo_site','1', isDemoSite) }} +
    +
    +
    + {# send email messages #} +
    -
    diff --git a/resources/views/admin/users/edit.twig b/resources/views/admin/users/edit.twig new file mode 100644 index 0000000000..0e7a531fc3 --- /dev/null +++ b/resources/views/admin/users/edit.twig @@ -0,0 +1,47 @@ +{% extends "./layout/default" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, user) }} +{% endblock %} +{% block content %} + + + {{ Form.model(user, {'class' : 'form-horizontal','id' : 'update','url' : route('admin.users.update',user.id) } ) }} + + + +
    +
    +
    +
    +

    {{ 'mandatoryFields'|_ }}

    +
    +
    + {{ ExpandedForm.text('email') }} + {{ ExpandedForm.password('password') }} + {{ ExpandedForm.password('password_confirmation') }} + {{ ExpandedForm.checkbox('blocked') }} + {{ ExpandedForm.select('blocked_code', codes, user.blocked_code) }} + +
    +
    +
    +
    + +
    +
    +

    {{ 'options'|_ }}

    +
    +
    + {{ ExpandedForm.optionsList('update','account') }} +
    + +
    +
    +
    + {{ Form.close|raw }} +{% endblock %} diff --git a/resources/views/admin/users/index.twig b/resources/views/admin/users/index.twig index 9b47a66dd8..a70b7cba77 100644 --- a/resources/views/admin/users/index.twig +++ b/resources/views/admin/users/index.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists }} @@ -11,71 +11,60 @@

    {{ 'all_users'|_ }}

    - +
    - - - - - - - - + + + + + + - + {% for user in users %} - - - + - - - - - - - - - - - - - - - - - - - - @@ -147,21 +143,23 @@ {{ session('end').formatLocalized(monthAndDayFormat) }} - + - {% if budget.otherRepetitions.count > 0 %} + {% if budgetInformation[budget.id]['otherLimits'].count > 0 %} @@ -185,8 +183,8 @@

    {{ 'inactiveBudgets'|_ }}

    - {% for index,budget in inactive %} - {% if index != inactive|length-1 %} + {% for budget in inactive %} + {% if loop.index == inactive.count() %} {{ budget.name }} {% else %} {{ budget.name }}, @@ -206,7 +204,7 @@ // budgeted data: var budgeted = {{ budgeted }}; - var budgetIncomeTotal = {{ budgetIncomeTotal }}; + var available = {{ available }}; diff --git a/resources/views/budgets/noBudget.twig b/resources/views/budgets/no-budget.twig similarity index 83% rename from resources/views/budgets/noBudget.twig rename to resources/views/budgets/no-budget.twig index 6abbb397d0..e8e1f0809b 100644 --- a/resources/views/budgets/noBudget.twig +++ b/resources/views/budgets/no-budget.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, subTitle) }} @@ -12,7 +12,7 @@

    {{ subTitle }}

    - {% include 'list.journals' with {'journals': list} %} + {% include 'list.journals-tasker' with {'journals': journals} %}
    diff --git a/resources/views/budgets/show.twig b/resources/views/budgets/show.twig index f88ab7ba9b..e1750adff8 100644 --- a/resources/views/budgets/show.twig +++ b/resources/views/budgets/show.twig @@ -1,25 +1,21 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} - {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, budget, repetition) }} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, budget, budgetLimit) }} {% endblock %} {% block content %}
    -
    +

    {{ 'overview'|_ }}

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

    {{ 'transactions'|_ }}

    - {% include 'list.journals' with {budgetPerspective: budget} %} + {% include 'list.journals-tasker' %}
    -
    - {% if limits|length == 1 %} -

    {{ 'showEverything'|_ }}

    - {% endif %} - +
    {% for limit in limits %} -
    +

    {{ limit.startdate.formatLocalized(monthFormat) }} + href="{{ route('budgets.show.limit',[budget.id,limit.id]) }}"> + {{ limit.start_date.formatLocalized(monthAndDayFormat) }} — + {{ limit.end_date.formatLocalized(monthAndDayFormat) }} +

    -
    -
    -
    - {{ 'amount'|_ }}: {{ limit.amount|formatAmount }} -
    -
    - {{ 'spent'|_ }}: {{ limit.spent|formatAmount }} -
    -
    -
    -
    - {% set overspent = limit.amount + limit.spent < 0 %} +
    +
     {{ trans('list.email') }}{{ trans('list.registered_at') }}{{ trans('list.registered_from') }}{{ trans('list.confirmed_from') }}{{ trans('list.is_admin') }}{{ trans('list.has_two_factor') }}{{ trans('list.is_activated') }}{{ trans('list.email') }} {{ trans('list.is_blocked') }}{{ trans('list.blocked_code') }}
    + #{{ user.id }} + {{ user.email }} + - {{ Preferences.getForUser(user,"registration_ip_address").data }} + - {{ Preferences.getForUser(user,"confirmation_ip_address").data }} - + + - {% if user.activated %} - - {% else %} - - {% endif %} - + {% if user.blocked == 1 %} {% else %} {% endif %} +
    {{ trans('list.registered_from') }}{{ registration }} ({{ registrationHost }})
    {{ trans('list.confirmed_from') }}{{ confirmation }} ({{ confirmationHost }})
    {{ trans('list.is_admin') }} @@ -56,16 +48,6 @@ {% endif %}
    {{ trans('list.is_activated') }} - {% if information.is_activated %} - Yes - {% else %} - No - {% endif %} -
    {{ trans('list.is_blocked') }} diff --git a/resources/views/attachments/delete.twig b/resources/views/attachments/delete.twig index a6191d5c6e..3e854f8bde 100644 --- a/resources/views/attachments/delete.twig +++ b/resources/views/attachments/delete.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute().getName(), attachment) }} diff --git a/resources/views/attachments/edit.twig b/resources/views/attachments/edit.twig index 976e3b2981..199b39b64f 100644 --- a/resources/views/attachments/edit.twig +++ b/resources/views/attachments/edit.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute().getName(), attachment) }} diff --git a/resources/views/auth/confirmation/error.twig b/resources/views/auth/confirmation/error.twig index d95de0b214..a417e4db7b 100644 --- a/resources/views/auth/confirmation/error.twig +++ b/resources/views/auth/confirmation/error.twig @@ -12,8 +12,8 @@ - - {% include('partials/favicons.twig') %} + {# favicons #} + {% include('partials.favicons') %} diff --git a/resources/views/auth/confirmation/no-resent.twig b/resources/views/auth/confirmation/no-resent.twig index 01e079cbad..64c24e95e0 100644 --- a/resources/views/auth/confirmation/no-resent.twig +++ b/resources/views/auth/confirmation/no-resent.twig @@ -12,8 +12,8 @@ - - {% include('partials/favicons.twig') %} + {# favicons #} + {% include('partials.favicons') %} diff --git a/resources/views/auth/confirmation/resent.twig b/resources/views/auth/confirmation/resent.twig index c7e0307d3d..accae395df 100644 --- a/resources/views/auth/confirmation/resent.twig +++ b/resources/views/auth/confirmation/resent.twig @@ -12,8 +12,8 @@ - - {% include('partials/favicons.twig') %} + {# favicons #} + {% include('partials.favicons') %} diff --git a/resources/views/auth/login.twig b/resources/views/auth/login.twig index 435870685a..88353375a6 100644 --- a/resources/views/auth/login.twig +++ b/resources/views/auth/login.twig @@ -1,6 +1,17 @@ -{% extends "./layout/guest.twig" %} +{% extends "./layout/guest" %} {% block content %} + {% if IS_DEMO_SITE %} +
    +
    +

    + Welcome to the Firefly III demonstration website!
    +
    + To log in, please use email address {{ DEMO_USERNAME }} with password {{ DEMO_PASSWORD }} +

    +
    +
    + {% endif %} {% if errors.has('email') %}
    diff --git a/resources/views/auth/lost-two-factor.twig b/resources/views/auth/lost-two-factor.twig index 7f64e57379..c82ffddd6c 100644 --- a/resources/views/auth/lost-two-factor.twig +++ b/resources/views/auth/lost-two-factor.twig @@ -12,8 +12,8 @@ - - {% include('partials/favicons.twig') %} + {# favicons #} + {% include('partials.favicons') %} diff --git a/resources/views/auth/passwords/email.twig b/resources/views/auth/passwords/email.twig index 3f9a286116..475f9bb7e6 100644 --- a/resources/views/auth/passwords/email.twig +++ b/resources/views/auth/passwords/email.twig @@ -1,4 +1,4 @@ -{% extends "./layout/guest.twig" %} +{% extends "./layout/guest" %} {% block content %} diff --git a/resources/views/auth/passwords/reset.twig b/resources/views/auth/passwords/reset.twig index c70b06e8a8..bbe9a732d8 100644 --- a/resources/views/auth/passwords/reset.twig +++ b/resources/views/auth/passwords/reset.twig @@ -1,4 +1,4 @@ -{% extends "./layout/guest.twig" %} +{% extends "./layout/guest" %} {% block content %} diff --git a/resources/views/auth/register.twig b/resources/views/auth/register.twig index fc3f7bf51b..30431d4763 100644 --- a/resources/views/auth/register.twig +++ b/resources/views/auth/register.twig @@ -1,4 +1,4 @@ -{% extends "./layout/guest.twig" %} +{% extends "./layout/guest" %} {% block content %} @@ -17,7 +17,7 @@
    - {% if showDemoWarning %} + {% if isDemoSite %} {% endif %} @@ -27,10 +27,6 @@
    - {% if showDemoWarning %} -

    You will receive an email from Firefly III. If your email address - is incorrect, your account may not work.

    - {% endif %}
    diff --git a/resources/views/auth/two-factor.twig b/resources/views/auth/two-factor.twig index e7148d811e..ea4275aae2 100644 --- a/resources/views/auth/two-factor.twig +++ b/resources/views/auth/two-factor.twig @@ -1,4 +1,4 @@ -{% extends "./layout/guest.twig" %} +{% extends "./layout/guest" %} {% block content %} @@ -18,7 +18,7 @@ -
    +
    @@ -33,7 +33,7 @@
    - {{ 'two_factor_forgot'|_ }} + {{ 'two_factor_forgot'|_ }}
    {% endblock %} diff --git a/resources/views/bills/create.twig b/resources/views/bills/create.twig index eb1a970b22..071771ab32 100644 --- a/resources/views/bills/create.twig +++ b/resources/views/bills/create.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, piggyBank) }} @@ -61,7 +61,12 @@ {% block styles %} + + {% endblock %} {% block scripts %} + + + {% endblock %} diff --git a/resources/views/bills/delete.twig b/resources/views/bills/delete.twig index d9185aa759..650242af27 100644 --- a/resources/views/bills/delete.twig +++ b/resources/views/bills/delete.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, bill) }} @@ -24,8 +24,8 @@

    - {% if bill.transactionjournals|length > 0 %} - {{ Lang.choice('form.bill_keep_transactions', bill.transactionjournals|length,{count: bill.transactionjournals|length}) }} + {% if bill.transactionjournals.count > 0 %} + {{ Lang.choice('form.bill_keep_transactions', bill.transactionjournals.count,{count: bill.transactionjournals.count}) }} {% endif %}

    diff --git a/resources/views/bills/edit.twig b/resources/views/bills/edit.twig index ed8d331b82..dcda9c47e0 100644 --- a/resources/views/bills/edit.twig +++ b/resources/views/bills/edit.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, bill) }} @@ -60,7 +60,12 @@ {% endblock %} {% block styles %} + + {% endblock %} {% block scripts %} + + + {% endblock %} diff --git a/resources/views/bills/index.twig b/resources/views/bills/index.twig index 644c0f243e..28bc6261fe 100644 --- a/resources/views/bills/index.twig +++ b/resources/views/bills/index.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} @@ -22,7 +22,7 @@
    - {% include 'list/bills.twig' %} + {% include 'list/bills' %}
    diff --git a/resources/views/bills/show.twig b/resources/views/bills/show.twig index 85dd232568..9227d99d46 100644 --- a/resources/views/bills/show.twig +++ b/resources/views/bills/show.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, bill) }} @@ -106,7 +106,7 @@

    {{ 'connected_journals'|_ }}

    - {% include 'list.journals' %} + {% include 'list.journals-tasker' %}
    @@ -116,9 +116,10 @@ {% block scripts %} + diff --git a/resources/views/budgets/create.twig b/resources/views/budgets/create.twig index 24f47d7ac2..3974791a23 100644 --- a/resources/views/budgets/create.twig +++ b/resources/views/budgets/create.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} diff --git a/resources/views/budgets/delete.twig b/resources/views/budgets/delete.twig index 10601310ee..74590ff0c5 100644 --- a/resources/views/budgets/delete.twig +++ b/resources/views/budgets/delete.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, budget) }} @@ -25,8 +25,8 @@

    - {% if budget.transactionjournals|length > 0 %} - {{ Lang.choice('form.budget_keep_transactions', budget.transactionjournals|length, {count: budget.transactionjournals|length}) }} + {% if budget.transactionjournals.count > 0 %} + {{ Lang.choice('form.budget_keep_transactions', budget.transactionjournals.count, {count: budget.transactionjournals.count }) }} {% endif %}

    diff --git a/resources/views/budgets/edit.twig b/resources/views/budgets/edit.twig index 0bf5ec3daa..e8010bc424 100644 --- a/resources/views/budgets/edit.twig +++ b/resources/views/budgets/edit.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, budget) }} diff --git a/resources/views/budgets/income.twig b/resources/views/budgets/income.twig index aef412667c..28904dc66d 100644 --- a/resources/views/budgets/income.twig +++ b/resources/views/budgets/income.twig @@ -4,17 +4,18 @@ -
    +
    {{ trans('firefly.available_between',{start : periodStart, end: periodEnd }) }}: - {{ budgetIncomeTotal|formatAmount }} + {{ available|formatAmountPlain }}
    @@ -65,7 +65,7 @@

    - + {{ trans('firefly.transactions_no_budget', {start: periodStart, end: periodEnd }) }}

    @@ -90,8 +90,8 @@

    - {% if budget.currentRep.id %} - {{ budget.name }} {% else %} {{ budget.name }} @@ -122,20 +122,16 @@
    {{ defaultCurrency.symbol|raw }}
    - - + {% if budgetInformation[budget.id]['currentLimit'] %} + {% set repAmount = budgetInformation[budget.id]['currentLimit'].amount %} + {% else %} + {% set repAmount = '0' %} + {% endif %} +
    -

    {{ budget.spent|formatAmount }} + {{ budgetInformation[budget.id]['spent']|formatAmount }} +
      - {% for other in budget.otherRepetitions %} - {% if other.id != budget.currentRep.id %} -
    • Budgeted - {{ other.amount|formatAmountPlain }} - between - {{ other.startdate.formatLocalized(monthAndDayFormat) }} - and {{ other.enddate.formatLocalized(monthAndDayFormat) }}. -
    • - {% endif %} + {% for other in budgetInformation[budget.id]['otherLimits'] %} +
    • + + Budgeted + {{ other.amount|formatAmountPlain }} + between + {{ other.start_date.formatLocalized(monthAndDayFormat) }} + and {{ other.end_date.formatLocalized(monthAndDayFormat) }}. +
    • {% endfor %}
    + + + + + + + + + + + +
    {{ 'amount'|_ }}{{ limit.amount|formatAmount }}
    {{ 'spent'|_ }}{{ limit.spent|formatAmount }}
    + {% set overspent = limit.amount + limit.spent < 0 %} - {% if overspent %} - {% set pct = (limit.spent != 0 ? (limit.amount / (limit.spent*-1))*100 : 0) %} -
    -
    -
    -
    - {% else %} - {% set pct = (limit.amount != 0 ? (((limit.spent*-1) / limit.amount)*100) : 0) %} -
    -
    -
    - {% endif %} - - + {% if overspent %} + {% set pct = (limit.spent != 0 ? (limit.amount / (limit.spent*-1))*100 : 0) %} +
    +
    +
    +
    + {% else %} + {% set pct = (limit.amount != 0 ? (((limit.spent*-1) / limit.amount)*100) : 0) %} +
    +
    +
    + {% endif %} +
    {% endfor %} - - {% if limits|length == 1 %} -

    {{ 'showEverything'|_ }}

    - {% endif %} - +

    {{ 'showEverything'|_ }}

    @@ -97,16 +101,17 @@ {% block scripts %} + diff --git a/resources/views/categories/create.twig b/resources/views/categories/create.twig index a7911555fd..e99d27b20a 100644 --- a/resources/views/categories/create.twig +++ b/resources/views/categories/create.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} diff --git a/resources/views/categories/delete.twig b/resources/views/categories/delete.twig index 858af38417..9766ddb6f0 100644 --- a/resources/views/categories/delete.twig +++ b/resources/views/categories/delete.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, category) }} @@ -24,8 +24,8 @@

    - {% if category.transactionjournals|length > 0 %} - {{ Lang.choice('form.category_keep_transactions', category.transactionjournals|length, {count: category.transactionjournals|length}) }} + {% if category.transactionjournals.count > 0 %} + {{ Lang.choice('form.category_keep_transactions', category.transactionjournals.count, {count: category.transactionjournals.count }) }} {% endif %}

    diff --git a/resources/views/categories/edit.twig b/resources/views/categories/edit.twig index 5e40a3254e..3a465adf1b 100644 --- a/resources/views/categories/edit.twig +++ b/resources/views/categories/edit.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, category) }} diff --git a/resources/views/categories/index.twig b/resources/views/categories/index.twig index 95428b52a3..d4cfc7c335 100644 --- a/resources/views/categories/index.twig +++ b/resources/views/categories/index.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} @@ -23,7 +23,7 @@
    - {% include 'list/categories.twig' %} + {% include 'list/categories' %}
    @@ -35,6 +35,7 @@ {% block scripts %} + diff --git a/resources/views/categories/noCategory.twig b/resources/views/categories/no-category.twig similarity index 84% rename from resources/views/categories/noCategory.twig rename to resources/views/categories/no-category.twig index 3f43588b4c..c302ca0203 100644 --- a/resources/views/categories/noCategory.twig +++ b/resources/views/categories/no-category.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, subTitle) }} @@ -12,7 +12,7 @@ {{ subTitle }}
    - {% include 'list.journals' with {'journals': list} %} + {% include 'list.journals-tasker' %}
    diff --git a/resources/views/categories/show.twig b/resources/views/categories/show.twig index f80113b6cc..c798a6b587 100644 --- a/resources/views/categories/show.twig +++ b/resources/views/categories/show.twig @@ -1,81 +1,138 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} - {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, category) }} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, category, start, end) }} {% endblock %} {% block content %}
    -
    -
    -
    -

    {{ 'overview'|_ }} (month)

    -
    -
    - + {% if method == 'default' %} + {# both charts #} +
    +
    +
    +

    {{ 'overview'|_ }} ({{ 'per_period'|_|lower }})

    +
    +
    + +
    -
    -
    -
    -
    -

    {{ 'overview'|_ }} (all)

    -
    -
    - +
    +
    +
    +

    {{ 'overview'|_ }} ({{ 'all_periods'|_|lower }})

    +
    +
    + +
    -
    + {% endif %} + {% if method == 'date' %} + {# single chart #} +
    +
    +
    +

    {{ 'overview'|_ }} ({{ 'current_period'|_|lower }})

    +
    +
    + +
    +
    +
    + {% endif %} + {% if method == 'all' %} + {# all chart #} +
    +
    +
    +

    {{ 'overview'|_ }} ({{ 'all_periods'|_|lower }})

    +
    +
    + +
    +
    +
    + {% endif %}
    + {% if entries %} + + {% endif %} +
    -
    +

    {{ 'transactions'|_ }}

    - {% include 'list.journals' %} + {% include 'list.journals-tasker' with {hideCategories: true} %} + {% if entries %} +

    + + + {{ 'show_all_no_filter'|_ }} + +

    + {% else %} +

    + + + {{ 'show_the_current_period_and_overview'|_ }} + +

    + {% endif %}
    -
    - {% for entry in entries %} - {% if entry[2] != 0 or entry[3] != 0 %} -
    -
    -

    {{ entry[1] }} -

    + {% if entries %} +
    + {% for entry in entries %} + {% if entry[2] != 0 or entry[3] != 0 %} +
    + +
    + + {% if entry[2] != 0 %} + + + + + {% endif %} + {% if entry[3] != 0 %} + + + + + {% endif %} +
    {{ 'spent'|_ }}{{ entry[2]|formatAmount }}
    {{ 'earned'|_ }}{{ entry[3]|formatAmount }}
    +
    -
    - - {% if entry[2] != 0 %} - - - - - {% endif %} - {% if entry[3] != 0 %} - - - - - {% endif %} -
    {{ 'spent'|_ }}{{ entry[2]|formatAmount }}
    {{ 'earned'|_ }}{{ entry[3]|formatAmount }}
    -
    -
    - {% endif %} + {% endif %} - {% endfor %} -
    + {% endfor %} +
    + {% endif %}
    {% endblock %} {% block scripts %} + diff --git a/resources/views/categories/show_with_date.twig b/resources/views/categories/show_with_date.twig deleted file mode 100644 index 4e1cb77b81..0000000000 --- a/resources/views/categories/show_with_date.twig +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "./layout/default.twig" %} - -{% block breadcrumbs %} - {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, category, carbon) }} -{% endblock %} - -{% block content %} - -
    -
    -
    -
    -

    {{ 'overview'|_ }} (period)

    -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -

    {{ 'transactions'|_ }}

    -
    -
    - {% include 'list.journals' %} -
    -
    -
    -
    - -{% endblock %} -{% block scripts %} - - - - - -{% endblock %} diff --git a/resources/views/csv/column-roles.twig b/resources/views/csv/column-roles.twig index f25a0a2ea7..92b2590df7 100644 --- a/resources/views/csv/column-roles.twig +++ b/resources/views/csv/column-roles.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} diff --git a/resources/views/csv/download-config.twig b/resources/views/csv/download-config.twig index bd317bea7c..1dd7a94795 100644 --- a/resources/views/csv/download-config.twig +++ b/resources/views/csv/download-config.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} diff --git a/resources/views/csv/index.twig b/resources/views/csv/index.twig index cfa3f6450a..141566dff6 100644 --- a/resources/views/csv/index.twig +++ b/resources/views/csv/index.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} diff --git a/resources/views/csv/map.twig b/resources/views/csv/map.twig index f8562205bd..3f1cca1587 100644 --- a/resources/views/csv/map.twig +++ b/resources/views/csv/map.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} diff --git a/resources/views/csv/process.twig b/resources/views/csv/process.twig index 8ca39b64c9..2561268912 100644 --- a/resources/views/csv/process.twig +++ b/resources/views/csv/process.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} diff --git a/resources/views/currency/create.twig b/resources/views/currencies/create.twig similarity index 85% rename from resources/views/currency/create.twig rename to resources/views/currencies/create.twig index 70943d3e15..18854f366c 100644 --- a/resources/views/currency/create.twig +++ b/resources/views/currencies/create.twig @@ -1,11 +1,11 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} {% endblock %} {% block content %} - +
    @@ -17,6 +17,7 @@ {{ ExpandedForm.text('name',null,{'maxlength' : 48}) }} {{ ExpandedForm.text('symbol',null,{'maxlength': 8}) }} {{ ExpandedForm.text('code',null,{'maxlength' : 3}) }} + {{ ExpandedForm.integer('decimal_places',2,{'maxlength' : 2,'min': 0,'max': 12}) }}
    diff --git a/resources/views/currency/delete.twig b/resources/views/currencies/delete.twig similarity index 88% rename from resources/views/currency/delete.twig rename to resources/views/currencies/delete.twig index 492f33b6e0..e14902a17f 100644 --- a/resources/views/currency/delete.twig +++ b/resources/views/currencies/delete.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, currency) }} @@ -6,7 +6,7 @@ {% block content %} - +
    diff --git a/resources/views/currency/edit.twig b/resources/views/currencies/edit.twig similarity index 88% rename from resources/views/currency/edit.twig rename to resources/views/currencies/edit.twig index a39ab1b0e4..1c614ebbb2 100644 --- a/resources/views/currency/edit.twig +++ b/resources/views/currencies/edit.twig @@ -1,11 +1,11 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, currency) }} {% endblock %} {% block content %} - {{ Form.model(currency, {'class' : 'form-horizontal','id' : 'update','url' : route('currency.update',currency.id)}) }} + {{ Form.model(currency, {'class' : 'form-horizontal','id' : 'update','url' : route('currencies.update',currency.id)}) }}
    @@ -18,6 +18,7 @@ {{ ExpandedForm.text('name',null,{'maxlength' : 48}) }} {{ ExpandedForm.text('symbol',null,{'maxlength' : 8}) }} {{ ExpandedForm.text('code',null,{'maxlength' : 3}) }} + {{ ExpandedForm.integer('decimal_places',null,{'maxlength' : 2,'min': 0,'max': 12}) }}
    diff --git a/resources/views/currency/index.twig b/resources/views/currencies/index.twig similarity index 79% rename from resources/views/currency/index.twig rename to resources/views/currencies/index.twig index 4effcba0a5..6c26819d9c 100644 --- a/resources/views/currency/index.twig +++ b/resources/views/currencies/index.twig @@ -1,4 +1,4 @@ -{% extends "./layout/default.twig" %} +{% extends "./layout/default" %} {% block breadcrumbs %} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} @@ -20,7 +20,9 @@   - {{ 'currency'|_ }} + {{ 'currency'|_ }} + {{ 'number_of_decimals'|_ }} +   @@ -28,17 +30,18 @@
    - - + +
    {{ currency.name }} ({{ currency.code }}) ({{ currency.symbol|raw }}) + {{ currency.decimal_places }} {% if currency.id == defaultCurrency.id %} {{ 'default_currency'|_ }} {% else %} {{ 'make_default_currency'|_ }} + href="{{ route('currencies.default',currency.id) }}">{{ 'make_default_currency'|_ }} {% endif %} @@ -48,7 +51,7 @@ {% endif %}
    diff --git a/resources/views/demo/accounts/index.twig b/resources/views/demo/accounts/index.twig new file mode 100644 index 0000000000..9d3cf18a02 --- /dev/null +++ b/resources/views/demo/accounts/index.twig @@ -0,0 +1 @@ +{{ trans('demo.accounts-index') }} diff --git a/resources/views/demo/budgets/index.twig b/resources/views/demo/budgets/index.twig new file mode 100644 index 0000000000..5717ecfce4 --- /dev/null +++ b/resources/views/demo/budgets/index.twig @@ -0,0 +1 @@ +{{ trans('demo.budgets-index') }} diff --git a/resources/views/demo/currencies/index.twig b/resources/views/demo/currencies/index.twig new file mode 100644 index 0000000000..1ea152a0f2 --- /dev/null +++ b/resources/views/demo/currencies/index.twig @@ -0,0 +1 @@ +{{ trans('demo.currencies-index') }} diff --git a/resources/views/demo/home.twig b/resources/views/demo/home.twig new file mode 100644 index 0000000000..85268e795a --- /dev/null +++ b/resources/views/demo/home.twig @@ -0,0 +1 @@ +{{ trans('demo.index', {asset: route('accounts.index', ['asset']), budgets: route('budgets.index'), reports: route('reports.index')})|raw }} diff --git a/resources/views/demo/import/configure.twig b/resources/views/demo/import/configure.twig new file mode 100644 index 0000000000..a94b4914bb --- /dev/null +++ b/resources/views/demo/import/configure.twig @@ -0,0 +1,3 @@ +{{ trans('demo.import-configure-security') }} +

    +{{ trans('demo.import-configure-configuration') }} diff --git a/resources/views/demo/import/index.twig b/resources/views/demo/import/index.twig new file mode 100644 index 0000000000..31fe3ab386 --- /dev/null +++ b/resources/views/demo/import/index.twig @@ -0,0 +1 @@ +{{ trans('demo.import-index') }} diff --git a/resources/views/demo/index.twig b/resources/views/demo/index.twig new file mode 100644 index 0000000000..85268e795a --- /dev/null +++ b/resources/views/demo/index.twig @@ -0,0 +1 @@ +{{ trans('demo.index', {asset: route('accounts.index', ['asset']), budgets: route('budgets.index'), reports: route('reports.index')})|raw }} diff --git a/resources/views/demo/no-demo-text.twig b/resources/views/demo/no-demo-text.twig new file mode 100644 index 0000000000..1e7116dc6b --- /dev/null +++ b/resources/views/demo/no-demo-text.twig @@ -0,0 +1,3 @@ +{{ trans('demo.no_demo_text', {route: Route.getCurrentRoute.getName})|raw }} + +{{ trans('demo.see_help_icon')|raw }} diff --git a/resources/views/demo/piggy-banks/index.twig b/resources/views/demo/piggy-banks/index.twig new file mode 100644 index 0000000000..7c0415fc31 --- /dev/null +++ b/resources/views/demo/piggy-banks/index.twig @@ -0,0 +1 @@ +{{ trans('demo.piggy-banks-index') }} diff --git a/resources/views/demo/reports/index.twig b/resources/views/demo/reports/index.twig new file mode 100644 index 0000000000..535fb63c44 --- /dev/null +++ b/resources/views/demo/reports/index.twig @@ -0,0 +1,7 @@ +{{ trans('demo.reports-index-start')|raw }} +
    +
    +{{ trans('demo.reports-index-examples', { + one: route('reports.report.default', ['1,2,3','currentMonthStart','currentMonthEnd']), + two: route('reports.report.default', ['1,2,3','20160101','20161231']), + three: route('reports.report.budget', ['1,2,3','2,1','20160101','20161231'])})|raw }} diff --git a/resources/views/demo/transactions/index.twig b/resources/views/demo/transactions/index.twig new file mode 100644 index 0000000000..247ad41218 --- /dev/null +++ b/resources/views/demo/transactions/index.twig @@ -0,0 +1 @@ +{{ trans('demo.transactions-index') }} diff --git a/resources/views/emails/blocked-login-html.twig b/resources/views/emails/blocked-login-html.twig deleted file mode 100644 index 8c23391b25..0000000000 --- a/resources/views/emails/blocked-login-html.twig +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - -

    - Hey there, -

    -

    - Firefly III has just blocked a login from user #{{ user_id }} ({{ user_address }}). -

    -

    - The blocked code was "{{ code }}". -

    - -

    - The login attempt came from IP {{ ip }}. -

    - - - diff --git a/resources/views/emails/blocked-login.twig b/resources/views/emails/blocked-login.twig deleted file mode 100644 index 16402bd9e8..0000000000 --- a/resources/views/emails/blocked-login.twig +++ /dev/null @@ -1,7 +0,0 @@ -Hey there, - -Firefly III has just blocked a login from user #{{ user_id }} ({{ user_address }}). - -The blocked code was "{{ code }}". - -The login attempt came from IP {{ ip }} diff --git a/resources/views/emails/blocked-registration-html.twig b/resources/views/emails/blocked-registration-html.twig deleted file mode 100644 index ac78890963..0000000000 --- a/resources/views/emails/blocked-registration-html.twig +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - -

    - Hey there, -

    -

    - Firefly III has just blocked a registration for an email addres at domain {{ blocked_domain }}. -

    -

    - The blocked email address was "{{ email_address }}". -

    - -

    - The login attempt came from IP {{ ip }}. -

    - - - diff --git a/resources/views/emails/blocked-registration.twig b/resources/views/emails/blocked-registration.twig deleted file mode 100644 index 3d5a2cccde..0000000000 --- a/resources/views/emails/blocked-registration.twig +++ /dev/null @@ -1,7 +0,0 @@ -Hey there, - -Firefly III has just blocked a registration for an email addres at domain {{ blocked_domain }}. - -The blocked email address was "{{ email_address }}". - -The login attempt came from IP {{ ip }}. diff --git a/resources/views/emails/confirm-account-html.twig b/resources/views/emails/confirm-account-html.twig index 147f6d455e..9dacd4e6a5 100644 --- a/resources/views/emails/confirm-account-html.twig +++ b/resources/views/emails/confirm-account-html.twig @@ -1,25 +1,14 @@ - - - - - - - - -

    - Hey there! -

    - +{% include 'emails.header-html' %}

    To start using your brand new Firefly III account, you need to activate it. Activating your account allows the website to verify that this email address is valid. Unfortunately, not even the most complex routines can work this out, without actually sending an email message. And here it is!

    - To active your account, click on this link. + To active your account please follow the link below.

    -

    - If the link does not work, you can browse there manually by copy/pasting it: +

    + PLEASE verify that this activation link goes to the Firefly III installation you expect it to be:

    {{ route }} @@ -28,15 +17,4 @@

    You should be redirected to the index page right away. The link expires in about four hours.

    - -

    - Thank you, and enjoy! -

    - -

    - James Cole -

    - -

    - You are getting this activation message because a new registration from IP {{ ip }} triggered it. -

    +{% include 'emails.footer-html' %} diff --git a/resources/views/emails/confirm-account.twig b/resources/views/emails/confirm-account-text.twig similarity index 62% rename from resources/views/emails/confirm-account.twig rename to resources/views/emails/confirm-account-text.twig index 3fa0b9be14..ca8f3a1e1b 100644 --- a/resources/views/emails/confirm-account.twig +++ b/resources/views/emails/confirm-account-text.twig @@ -1,16 +1,11 @@ -Hey there! - +{% include 'emails.header-text' %} To start using your brand new Firefly III account, you need to activate it. Activating your account allows the website to verify that this email address is valid. Unfortunately, not even the most complex routines can work this out, without actually sending an email message. And here it is! -To active your account, you can click on the following link, or copy/paste it in your web browser: +To active your account please follow the link below. + +PLEASE verify that this activation link goes to the Firefly III installation you expect it to be: {{ route }} You should be redirected to the index page right away. The link expires in about four hours. - -Thank you, and enjoy! - -James Cole - - -You are getting this activation message because a new registration from IP {{ ip }} triggered it. +{% include 'emails.footer-text' %} diff --git a/resources/views/emails/error-html.blade.php b/resources/views/emails/error-html.blade.php deleted file mode 100644 index b8f186249f..0000000000 --- a/resources/views/emails/error-html.blade.php +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - -

    - Firefly III ran into an error: {{ errorMessage }} -

    - -

    - The error was of type "{{ class }}". -

    - -

    - The error occured on/at: {{ time }}. -

    - -

    - This error occured in file {{ file }} on line {{ line }} with code {{ code }}. -

    - -

    - {% if loggedIn %} - The error was encountered by user #{{ user.id }}, {{ user.email }}. - {% else %} - There was no user logged in for this error or no user was detected. - {% endif %} -

    - -

    - The IP address related to this error is: {{ ip }} -

    - -

    - The full stacktrace is below. If you think this is a bug in Firefly III, you - can forward this message to - thegrumpydictator@gmail.com. - This can help fix the bug you just encountered. -

    -

    - If you prefer, you can also open a new issue on Github. -

    - -

    - The full stacktrace is below:

    -

    - {{ stacktrace|nl2br }} -

    - - diff --git a/resources/views/emails/error-html.twig b/resources/views/emails/error-html.twig index d486c0a1d5..5594a2a7eb 100644 --- a/resources/views/emails/error-html.twig +++ b/resources/views/emails/error-html.twig @@ -1,10 +1,4 @@ - - - - - - - +{% include 'emails.header-html' %}

    Firefly III ran into an error: {{ errorMessage }}

    @@ -40,13 +34,12 @@ This can help fix the bug you just encountered.

    - If you prefer, you can also open a new issue on Github. + If you prefer, you can also open a new issue on Github.

    The full stacktrace is below:

    - {{ stacktrace|nl2br }} + {{ stackTrace|nl2br }}

    - - +{% include 'emails.footer-html' %} diff --git a/resources/views/emails/error.twig b/resources/views/emails/error-text.twig similarity index 83% rename from resources/views/emails/error.twig rename to resources/views/emails/error-text.twig index b772919798..e195cdb631 100644 --- a/resources/views/emails/error.twig +++ b/resources/views/emails/error-text.twig @@ -1,3 +1,4 @@ +{% include 'emails.header-text' %} Firefly III ran into an error: {{ errorMessage }}. The error was of type "{{ class }}". @@ -20,8 +21,9 @@ the bug you just encountered. If you prefer, you can also open a new issue here: -https://github.com/JC5/firefly-iii/issues/new +https://github.com/firefly-iii/firefly-iii/issues The full stacktrace is below: -{{ stacktrace }} +{{ stackTrace }} +{% include 'emails.footer-text' %} diff --git a/resources/views/emails/footer-html.twig b/resources/views/emails/footer-html.twig new file mode 100644 index 0000000000..6f4f219380 --- /dev/null +++ b/resources/views/emails/footer-html.twig @@ -0,0 +1,13 @@ +

    + Beep boop, +

    +

    + The Firefly III Mail Robot +

    + +

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

    + + + diff --git a/resources/views/emails/footer-text.twig b/resources/views/emails/footer-text.twig new file mode 100644 index 0000000000..f3ed37bbbd --- /dev/null +++ b/resources/views/emails/footer-text.twig @@ -0,0 +1,6 @@ + +Beep boop, + +The Firefly III Mail Robot + +PS: This message was sent because a request from IP {{ ip }} triggered it. diff --git a/resources/views/emails/header-html.twig b/resources/views/emails/header-html.twig new file mode 100644 index 0000000000..f3a4e8edc7 --- /dev/null +++ b/resources/views/emails/header-html.twig @@ -0,0 +1,10 @@ + + + + + + + +

    + Hi there, +

    diff --git a/resources/views/emails/header-text.twig b/resources/views/emails/header-text.twig new file mode 100644 index 0000000000..e60df7e024 --- /dev/null +++ b/resources/views/emails/header-text.twig @@ -0,0 +1,2 @@ +Hi there, + diff --git a/resources/views/emails/password-html.twig b/resources/views/emails/password-html.twig new file mode 100644 index 0000000000..07e4e0a80a --- /dev/null +++ b/resources/views/emails/password-html.twig @@ -0,0 +1,13 @@ +{% include 'emails.header-html' %} +

    + Somebody tried to reset your password. If it was you, please follow the link below to do so. +

    + +

    + PLEASE verify that the link actually goes to the Firefly III you expect it to go! +

    + +

    + {{ url }} +

    +{% include 'emails.footer-html' %} diff --git a/resources/views/emails/password-text.twig b/resources/views/emails/password-text.twig new file mode 100644 index 0000000000..0e67dd735b --- /dev/null +++ b/resources/views/emails/password-text.twig @@ -0,0 +1,7 @@ +{% include 'emails.header-text' %} +Somebody tried to reset your password. If it was you, please follow the link below to do so. + +PLEASE verify that the link actually goes to the Firefly III you expect it to go! + +{{ url }} +{% include 'emails.footer-text' %} diff --git a/resources/views/emails/password.twig b/resources/views/emails/password.twig deleted file mode 100644 index d2be49d030..0000000000 --- a/resources/views/emails/password.twig +++ /dev/null @@ -1 +0,0 @@ -Click here to reset your password: {{ url('password/reset/' ~ token) }} diff --git a/resources/views/emails/registered-html.twig b/resources/views/emails/registered-html.twig index 02d2407d94..4aec81c009 100644 --- a/resources/views/emails/registered-html.twig +++ b/resources/views/emails/registered-html.twig @@ -1,14 +1,4 @@ - - - - - - - -

    - Hey there, -

    - +{% include 'emails.header-html' %}

    Welkome to Firefly III. Your registration has made it, and this email is here to confirm it. Yay!

    @@ -22,26 +12,7 @@ There is a help-icon in the top right corner of each page. If you need help, click it!
  • - If you haven't already, please read the - first use guide and the - full description. -
  • -
  • - If this installation of Firefly is configured to send activation mails as well, you should get an activation - link very soon! + If you haven't already, please read the full description.
  • - -

    - Enjoy! -

    -

    - James Cole -

    - -

    - The registration has been created from IP {{ ip }} -

    - - - +{% include 'emails.footer-html' %} diff --git a/resources/views/emails/registered.twig b/resources/views/emails/registered-text.twig similarity index 74% rename from resources/views/emails/registered.twig rename to resources/views/emails/registered-text.twig index 66dfca0eb9..4877bf52fd 100644 --- a/resources/views/emails/registered.twig +++ b/resources/views/emails/registered-text.twig @@ -1,5 +1,4 @@ -Hey there, - +{% include 'emails.header-text' %} Welkome to Firefly III. Your registration has made it, and this email is here to confirm it. Yay! * If you have forgotten your password already, please reset it using the password reset tool. @@ -15,7 +14,7 @@ Password reset: {{ address }}/password/reset Documentation: -https://github.com/JC5/firefly-iii/wiki/First-use -http://jc5.github.io/firefly-iii//description/ +https://github.com/firefly-iii/firefly-iii +https://firefly-iii.github.io/ -The registration has been created from IP {{ ip }} +{% include 'emails.footer-text' %} diff --git a/resources/views/error.twig b/resources/views/error.twig index 4390f9c05c..a41611d4f3 100644 --- a/resources/views/error.twig +++ b/resources/views/error.twig @@ -1,4 +1,4 @@ -{% extends "./layout/guest.twig" %} +{% extends "./layout/guest" %} {% block content %} @@ -10,7 +10,12 @@
    - {{ message |default('General unknown errror') }} +

    {{ message |default('General unknown errror') }}

    +
    +
    + {% endblock %} diff --git a/resources/views/errors/404.twig b/resources/views/errors/404.twig index 41481b8501..d84ff35ca6 100644 --- a/resources/views/errors/404.twig +++ b/resources/views/errors/404.twig @@ -1,254 +1,47 @@ - - - + + + 404 + + + + + + + + {% include('partials.favicons') %} - -
    -

    Sorry, the page you are looking for could not be found.

    + +
    + +
    +
    +

    404 — Firefly III cannot find this page.

    +
    +
    + +
    +
    +

    + The page you have requested does not exist. Please check that you have not entered + the wrong URL. Did you make a typo perhaps? +

    +
    +
    +
    +
    +

    + If you are sure this page should exist, please open a ticket on + Github. +

    +
    +
    + diff --git a/resources/views/errors/500.twig b/resources/views/errors/500.twig index 0fd75c042e..456f0b73dd 100644 --- a/resources/views/errors/500.twig +++ b/resources/views/errors/500.twig @@ -1,8 +1,17 @@ - - +