aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoris2021-01-03 13:40:40 +0100
committerJoris2021-01-03 13:54:20 +0100
commit11052951b74b9ad4b6a9412ae490086235f9154b (patch)
tree64526ac926c1bf470ea113f6cac8a33158684e8d
parent371449b0e312a03162b78797b83dee9d81706669 (diff)
downloadbudget-11052951b74b9ad4b6a9412ae490086235f9154b.tar.gz
budget-11052951b74b9ad4b6a9412ae490086235f9154b.tar.bz2
budget-11052951b74b9ad4b6a9412ae490086235f9154b.zip
Rewrite in Rust
-rw-r--r--.gitignore13
-rw-r--r--.rustfmt.toml2
-rw-r--r--.stylish-haskell.yaml34
-rw-r--r--.tmuxinator.yml14
-rw-r--r--Cargo.lock2675
-rw-r--r--Cargo.toml26
-rw-r--r--Makefile62
-rw-r--r--README.md68
-rw-r--r--application.conf9
-rw-r--r--assets/chart.js7
-rw-r--r--assets/icon.png (renamed from public/images/icon.png)bin1203 -> 1203 bytes
-rw-r--r--assets/main.css399
-rw-r--r--assets/main.js226
-rwxr-xr-xbin/db26
-rwxr-xr-xbin/watch15
-rw-r--r--cabal-client.project4
-rw-r--r--cabal-server.project3
-rw-r--r--client/LICENSE674
-rw-r--r--client/Setup.hs2
-rw-r--r--client/client.cabal90
-rw-r--r--client/src/Component/Appearing.hs10
-rw-r--r--client/src/Component/Button.hs57
-rw-r--r--client/src/Component/ConfirmDialog.hs49
-rw-r--r--client/src/Component/Form.hs12
-rw-r--r--client/src/Component/Input.hs151
-rw-r--r--client/src/Component/Link.hs33
-rw-r--r--client/src/Component/Modal.hs117
-rw-r--r--client/src/Component/ModalForm.hs71
-rw-r--r--client/src/Component/Pages.hs86
-rw-r--r--client/src/Component/Select.hs80
-rw-r--r--client/src/Component/Table.hs105
-rw-r--r--client/src/Component/Tag.hs27
-rw-r--r--client/src/Loadable.hs109
-rw-r--r--client/src/Main.hs39
-rw-r--r--client/src/Model/Route.hs11
-rw-r--r--client/src/Util/Ajax.hs139
-rw-r--r--client/src/Util/Css.hs9
-rw-r--r--client/src/Util/Either.hs7
-rw-r--r--client/src/Util/Reflex.hs59
-rw-r--r--client/src/Util/Router.hs266
-rw-r--r--client/src/Util/Validation.hs36
-rw-r--r--client/src/Util/WaitFor.hs17
-rw-r--r--client/src/View/App.hs108
-rw-r--r--client/src/View/Category/Category.hs94
-rw-r--r--client/src/View/Category/Form.hs117
-rw-r--r--client/src/View/Category/Reducer.hs59
-rw-r--r--client/src/View/Category/Table.hs93
-rw-r--r--client/src/View/Header.hs123
-rw-r--r--client/src/View/Icon.hs71
-rw-r--r--client/src/View/Income/Form.hs119
-rw-r--r--client/src/View/Income/Header.hs77
-rw-r--r--client/src/View/Income/Income.hs75
-rw-r--r--client/src/View/Income/Reducer.hs59
-rw-r--r--client/src/View/Income/Table.hs93
-rw-r--r--client/src/View/NotFound.hs20
-rw-r--r--client/src/View/Payment/Form.hs199
-rw-r--r--client/src/View/Payment/HeaderForm.hs85
-rw-r--r--client/src/View/Payment/HeaderInfos.hs94
-rw-r--r--client/src/View/Payment/Payment.hs101
-rw-r--r--client/src/View/Payment/Reducer.hs110
-rw-r--r--client/src/View/Payment/Table.hs145
-rw-r--r--client/src/View/SignIn.hs82
-rw-r--r--client/src/View/Statistics/Chart.hs102
-rw-r--r--client/src/View/Statistics/Statistics.hs85
-rw-r--r--common/LICENSE674
-rw-r--r--common/Setup.hs2
-rw-r--r--common/common.cabal72
-rw-r--r--common/src/Common/Message/Key.hs150
-rw-r--r--common/src/Common/Message/Lang.hs7
-rw-r--r--common/src/Common/Message/Translation.hs655
-rw-r--r--common/src/Common/Model.hs26
-rw-r--r--common/src/Common/Model/Category.hs24
-rw-r--r--common/src/Common/Model/CategoryPage.hs18
-rw-r--r--common/src/Common/Model/CreateCategoryForm.hs15
-rw-r--r--common/src/Common/Model/CreateIncomeForm.hs15
-rw-r--r--common/src/Common/Model/CreatePaymentForm.hs21
-rw-r--r--common/src/Common/Model/Currency.hs12
-rw-r--r--common/src/Common/Model/EditCategoryForm.hs18
-rw-r--r--common/src/Common/Model/EditIncome.hs17
-rw-r--r--common/src/Common/Model/EditIncomeForm.hs18
-rw-r--r--common/src/Common/Model/EditPaymentForm.hs23
-rw-r--r--common/src/Common/Model/Email.hs12
-rw-r--r--common/src/Common/Model/ExceedingPayer.hs16
-rw-r--r--common/src/Common/Model/Frequency.hs14
-rw-r--r--common/src/Common/Model/Income.hs27
-rw-r--r--common/src/Common/Model/IncomeHeader.hs18
-rw-r--r--common/src/Common/Model/IncomePage.hs19
-rw-r--r--common/src/Common/Model/Init.hs18
-rw-r--r--common/src/Common/Model/Password.hs12
-rw-r--r--common/src/Common/Model/Payment.hs33
-rw-r--r--common/src/Common/Model/PaymentHeader.hs18
-rw-r--r--common/src/Common/Model/PaymentPage.hs21
-rw-r--r--common/src/Common/Model/SignInForm.hs15
-rw-r--r--common/src/Common/Model/Stats.hs23
-rw-r--r--common/src/Common/Model/User.hs27
-rw-r--r--common/src/Common/Msg.hs13
-rw-r--r--common/src/Common/Util/Text.hs49
-rw-r--r--common/src/Common/Util/Time.hs26
-rw-r--r--common/src/Common/Util/Validation.hs13
-rw-r--r--common/src/Common/Validation/Atomic.hs61
-rw-r--r--common/src/Common/Validation/Category.hs15
-rw-r--r--common/src/Common/Validation/Income.hs17
-rw-r--r--common/src/Common/Validation/Payment.hs31
-rw-r--r--common/src/Common/Validation/SignIn.hs17
-rw-r--r--common/src/Common/View/Format.hs78
-rw-r--r--config.json4
-rw-r--r--default.nix25
-rw-r--r--docs/balance.pngbin0 -> 61703 bytes
-rw-r--r--docs/payments.pngbin0 -> 141443 bytes
-rw-r--r--docs/statistics.pngbin0 -> 79563 bytes
-rw-r--r--nix/nixpkgs.nix6
-rw-r--r--nix/tools.nix15
-rw-r--r--public/css/reset.css60
-rw-r--r--public/javascript/.gitkeep0
-rw-r--r--server/LICENSE674
-rw-r--r--server/Setup.hs2
-rw-r--r--server/server.cabal131
-rw-r--r--server/src/Conf.hs39
-rw-r--r--server/src/Controller/Category.hs88
-rw-r--r--server/src/Controller/Helper.hs16
-rw-r--r--server/src/Controller/Income.hs90
-rw-r--r--server/src/Controller/Index.hs76
-rw-r--r--server/src/Controller/Payment.hs118
-rw-r--r--server/src/Controller/Statistics.hs21
-rw-r--r--server/src/Controller/User.hs17
-rw-r--r--server/src/Cookie.hs55
-rw-r--r--server/src/Design/Appearing.hs25
-rw-r--r--server/src/Design/Color.hs40
-rw-r--r--server/src/Design/Constants.hs27
-rw-r--r--server/src/Design/Errors.hs53
-rw-r--r--server/src/Design/Form.hs101
-rw-r--r--server/src/Design/Global.hs165
-rw-r--r--server/src/Design/Helper.hs48
-rw-r--r--server/src/Design/Loadable.hs29
-rw-r--r--server/src/Design/Media.hs36
-rw-r--r--server/src/Design/Modal.hs69
-rw-r--r--server/src/Design/Tooltip.hs14
-rw-r--r--server/src/Design/View/ConfirmDialog.hs36
-rw-r--r--server/src/Design/View/Header.hs93
-rw-r--r--server/src/Design/View/NotFound.hs21
-rw-r--r--server/src/Design/View/Pages.hs55
-rw-r--r--server/src/Design/View/Payment.hs15
-rw-r--r--server/src/Design/View/Payment/Add.hs35
-rw-r--r--server/src/Design/View/Payment/Form.hs35
-rw-r--r--server/src/Design/View/Payment/HeaderForm.hs40
-rw-r--r--server/src/Design/View/Payment/HeaderInfos.hs50
-rw-r--r--server/src/Design/View/SignIn.hs36
-rw-r--r--server/src/Design/View/Stat.hs17
-rw-r--r--server/src/Design/View/Table.hs99
-rw-r--r--server/src/Design/Views.hs56
-rw-r--r--server/src/Job/Daemon.hs37
-rw-r--r--server/src/Job/Frequency.hs13
-rw-r--r--server/src/Job/Kind.hs23
-rw-r--r--server/src/Job/Model.hs49
-rw-r--r--server/src/Job/MonthlyPayment.hs26
-rw-r--r--server/src/Job/WeeklyReport.hs52
-rw-r--r--server/src/LoginSession.hs52
-rw-r--r--server/src/Main.hs106
-rw-r--r--server/src/Model/CreateCategory.hs10
-rw-r--r--server/src/Model/CreateIncome.hs10
-rw-r--r--server/src/Model/CreatePayment.hs16
-rw-r--r--server/src/Model/EditCategory.hs13
-rw-r--r--server/src/Model/EditIncome.hs13
-rw-r--r--server/src/Model/EditPayment.hs17
-rw-r--r--server/src/Model/HashedPassword.hs27
-rw-r--r--server/src/Model/IncomeResource.hs15
-rw-r--r--server/src/Model/Mail.hs12
-rw-r--r--server/src/Model/PaymentResource.hs15
-rw-r--r--server/src/Model/Query.hs32
-rw-r--r--server/src/Model/SignIn.hs10
-rw-r--r--server/src/Model/UUID.hs10
-rw-r--r--server/src/Payer.hs87
-rw-r--r--server/src/Persistence/Category.hs123
-rw-r--r--server/src/Persistence/Frequency.hs23
-rw-r--r--server/src/Persistence/Income.hs201
-rw-r--r--server/src/Persistence/Payment.hs389
-rw-r--r--server/src/Persistence/User.hs78
-rw-r--r--server/src/Persistence/Util.hs11
-rw-r--r--server/src/Resource.hs54
-rw-r--r--server/src/Secure.hs31
-rw-r--r--server/src/SendMail.hs66
-rw-r--r--server/src/Statistics.hs59
-rw-r--r--server/src/Util/Time.hs22
-rw-r--r--server/src/Validation/Category.hs27
-rw-r--r--server/src/Validation/Income.hs27
-rw-r--r--server/src/Validation/Payment.hs33
-rw-r--r--server/src/Validation/SignIn.hs16
-rw-r--r--server/src/View/Mail/WeeklyReport.hs124
-rw-r--r--server/src/View/Page.hs43
-rw-r--r--shell.nix30
-rw-r--r--sql/fixtures.sql45
-rw-r--r--sql/migrations/1.sql (renamed from server/migrations/1.sql)0
-rw-r--r--sql/migrations/2.sql (renamed from server/migrations/2.sql)0
-rw-r--r--sql/migrations/3.sql (renamed from server/migrations/3.sql)0
-rw-r--r--sql/migrations/4.sql91
-rw-r--r--src/assets.rs27
-rw-r--r--src/controller/balance.rs71
-rw-r--r--src/controller/categories.rs141
-rw-r--r--src/controller/error.rs31
-rw-r--r--src/controller/incomes.rs221
-rw-r--r--src/controller/login.rs86
-rw-r--r--src/controller/mod.rs9
-rw-r--r--src/controller/payments.rs227
-rw-r--r--src/controller/statistics.rs30
-rw-r--r--src/controller/utils.rs119
-rw-r--r--src/controller/wallet.rs13
-rw-r--r--src/db/categories.rs132
-rw-r--r--src/db/incomes.rs494
-rw-r--r--src/db/jobs.rs56
-rw-r--r--src/db/mod.rs6
-rw-r--r--src/db/payments.rs525
-rw-r--r--src/db/users.rs144
-rw-r--r--src/db/utils.rs3
-rw-r--r--src/jobs/jobs.rs28
-rw-r--r--src/jobs/mod.rs2
-rw-r--r--src/jobs/weekly_report.rs55
-rw-r--r--src/mail.rs59
-rw-r--r--src/main.rs88
-rw-r--r--src/model/action.rs1
-rw-r--r--src/model/category.rs20
-rw-r--r--src/model/config.rs8
-rw-r--r--src/model/frequency.rs31
-rw-r--r--src/model/income.rs40
-rw-r--r--src/model/job.rs5
-rw-r--r--src/model/login.rs5
-rw-r--r--src/model/mod.rs9
-rw-r--r--src/model/payment.rs53
-rw-r--r--src/model/report.rs16
-rw-r--r--src/model/user.rs8
-rw-r--r--src/payer.rs38
-rw-r--r--src/queries.rs62
-rw-r--r--src/routes.rs219
-rw-r--r--src/templates.rs97
-rw-r--r--src/utils/mod.rs1
-rw-r--r--src/utils/text.rs19
-rw-r--r--src/validation/category.rs18
-rw-r--r--src/validation/income.rs22
-rw-r--r--src/validation/login.rs11
-rw-r--r--src/validation/mod.rs5
-rw-r--r--src/validation/payment.rs25
-rw-r--r--src/validation/utils.rs54
-rw-r--r--templates/balance.html107
-rw-r--r--templates/base.html91
-rw-r--r--templates/category/create.html51
-rw-r--r--templates/category/table.html38
-rw-r--r--templates/category/update.html85
-rw-r--r--templates/error.html17
-rw-r--r--templates/income/create.html89
-rw-r--r--templates/income/table.html55
-rw-r--r--templates/income/update.html117
-rw-r--r--templates/login.html29
-rw-r--r--templates/macros/paging.html55
-rw-r--r--templates/payment/create.html120
-rw-r--r--templates/payment/table.html128
-rw-r--r--templates/payment/update.html144
-rw-r--r--templates/report/list.j214
-rw-r--r--templates/report/report.j250
-rw-r--r--templates/statistics.html29
-rw-r--r--validation/LICENSE674
-rw-r--r--validation/Setup.hs2
-rw-r--r--validation/src/Data/Validation.hs375
-rw-r--r--validation/validation.cabal23
262 files changed, 8126 insertions, 12581 deletions
diff --git a/.gitignore b/.gitignore
index 19d08a2..c9f71b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,2 @@
-database
-database-shm
-database-wal
-dist-server
-dist-client
-local.conf
-public/javascript/main.js
-result-client
-result-server
-sessionKey
-.ghc.environment.*
+/database.db*
+/target
diff --git a/.rustfmt.toml b/.rustfmt.toml
new file mode 100644
index 0000000..0c2ca5c
--- /dev/null
+++ b/.rustfmt.toml
@@ -0,0 +1,2 @@
+edition = "2018"
+max_width = 80
diff --git a/.stylish-haskell.yaml b/.stylish-haskell.yaml
deleted file mode 100644
index 82305b9..0000000
--- a/.stylish-haskell.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-steps:
- - simple_align:
- cases: true
- top_level_patterns: true
- records: true
-
- - imports:
- align: global
- list_align: after_alias
- pad_module_names: true
- long_list_align: inline
- empty_list_align: inherit
- list_padding: 4
- separate_lists: true
- space_surround: false
-
- - language_pragmas:
- style: vertical
- align: true
- remove_redundant: true
-
- - trailing_whitespace: {}
-
-columns: 80
-
-newline: native
-
-language_extensions:
- - ExistentialQuantification
- - LambdaCase
- - MultiParamTypeClasses
- - OverloadedStrings
- - RecursiveDo
- - ScopedTypeVariables
diff --git a/.tmuxinator.yml b/.tmuxinator.yml
deleted file mode 100644
index 2a765c0..0000000
--- a/.tmuxinator.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: sharedCost
-startup_window: app
-
-windows:
- - console:
- - clear
- - app:
- panes:
- - server:
- - make watch-server
- - client:
- - make watch-client
- - db:
- - sqlite3 database
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..f40e23e
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,2675 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "ahash"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
+
+[[package]]
+name = "ahash"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75b7e6a93ecd6dbd2c225154d0fa7f86205574ecaa6c87429fb5f66ee677c44"
+dependencies = [
+ "getrandom 0.2.0",
+ "lazy_static",
+ "version_check 0.9.2",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "ascii_utils"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
+
+[[package]]
+name = "atoi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "autocfg"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base64"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643"
+dependencies = [
+ "byteorder",
+ "safemem",
+]
+
+[[package]]
+name = "base64"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "base64"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bcrypt"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4d0faafe9e089674fc3efdb311ff5253d445c79d85d1d28bd3ace76d45e7164"
+dependencies = [
+ "base64 0.13.0",
+ "blowfish",
+ "getrandom 0.2.0",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "bitvec"
+version = "0.19.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
+dependencies = [
+ "block-padding",
+ "byte-tools",
+ "byteorder",
+ "generic-array 0.12.3",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array 0.14.4",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
+dependencies = [
+ "byte-tools",
+]
+
+[[package]]
+name = "blowfish"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32fa6a061124e37baba002e496d203e23ba3d7b73750be82dbfbc92913048a5b"
+dependencies = [
+ "byteorder",
+ "cipher",
+ "opaque-debug 0.3.0",
+]
+
+[[package]]
+name = "bstr"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "473fc6b38233f9af7baa94fb5852dca389e3d95b8e21c8e3719301462c5d9faf"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "budget"
+version = "0.1.0"
+dependencies = [
+ "bcrypt",
+ "chrono",
+ "env_logger",
+ "hyper",
+ "lettre",
+ "lettre_email",
+ "log",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sha2",
+ "sqlx",
+ "sqlx-core",
+ "structopt",
+ "tera",
+ "tokio",
+ "tokio-util",
+ "url",
+ "uuid 0.8.1",
+]
+
+[[package]]
+name = "bufstream"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
+
+[[package]]
+name = "build_const"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
+
+[[package]]
+name = "bumpalo"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
+
+[[package]]
+name = "byte-tools"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
+
+[[package]]
+name = "byteorder"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
+
+[[package]]
+name = "bytes"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
+
+[[package]]
+name = "cargo_metadata"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83f95cf4bf0dda0ac2e65371ae7215d0dce3c187613a9dbf23aaa9374186f97a"
+dependencies = [
+ "semver",
+ "semver-parser",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "time",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "chrono-tz"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120"
+dependencies = [
+ "chrono",
+ "parse-zoneinfo",
+]
+
+[[package]]
+name = "cipher"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
+dependencies = [
+ "generic-array 0.14.4",
+]
+
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
+
+[[package]]
+name = "cpuid-bool"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
+
+[[package]]
+name = "crc"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
+dependencies = [
+ "build_const",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
+dependencies = [
+ "autocfg 1.0.1",
+ "cfg-if 1.0.0",
+ "lazy_static",
+]
+
+[[package]]
+name = "deunicode"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
+
+[[package]]
+name = "digest"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
+dependencies = [
+ "generic-array 0.12.3",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array 0.14.4",
+]
+
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "email"
+version = "0.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91549a51bb0241165f13d57fc4c72cef063b4088fb078b019ecbf464a45f22e4"
+dependencies = [
+ "base64 0.9.3",
+ "chrono",
+ "encoding",
+ "lazy_static",
+ "rand 0.4.6",
+ "time",
+ "version_check 0.1.5",
+]
+
+[[package]]
+name = "encoding"
+version = "0.2.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
+dependencies = [
+ "encoding-index-japanese",
+ "encoding-index-korean",
+ "encoding-index-simpchinese",
+ "encoding-index-singlebyte",
+ "encoding-index-tradchinese",
+]
+
+[[package]]
+name = "encoding-index-japanese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-korean"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-simpchinese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-singlebyte"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-tradchinese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding_index_tests"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
+
+[[package]]
+name = "env_logger"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "fake-simd"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+
+[[package]]
+name = "fast_chemail"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4"
+dependencies = [
+ "ascii_utils",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "fuchsia-zircon"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
+dependencies = [
+ "bitflags",
+ "fuchsia-zircon-sys",
+]
+
+[[package]]
+name = "fuchsia-zircon-sys"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+
+[[package]]
+name = "funty"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
+
+[[package]]
+name = "futures"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4caa2b2b68b880003057c1dd49f1ed937e38f22fcf6c212188a121f08cf40a65"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556"
+dependencies = [
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "futures-util"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project 1.0.2",
+ "pin-utils",
+ "proc-macro-hack",
+ "proc-macro-nested",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+dependencies = [
+ "typenum",
+ "version_check 0.9.2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee8025cf36f917e6a52cce185b7c7177689b838b7ec138364e50cc2277a56cf4"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "globset"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "globwalk"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
+dependencies = [
+ "bitflags",
+ "ignore",
+ "walkdir",
+]
+
+[[package]]
+name = "h2"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "tracing-futures",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
+dependencies = [
+ "ahash 0.4.7",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "heck"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35"
+
+[[package]]
+name = "hostname"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e"
+dependencies = [
+ "libc",
+ "winutil",
+]
+
+[[package]]
+name = "http"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84129d298a6d57d246960ff8eb831ca4af3f96d29e2e28848dae275408658e26"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "httparse"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
+
+[[package]]
+name = "httpdate"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47"
+
+[[package]]
+name = "humansize"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
+
+[[package]]
+name = "humantime"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c1ad908cc71012b7bea4d0c53ba96a8cba9962f048fa68d143376143d863b7a"
+
+[[package]]
+name = "hyper"
+version = "0.13.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ad767baac13b44d4529fcf58ba2cd0995e36e7b435bc5b039de6f47e880dbf"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project 1.0.2",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c"
+dependencies = [
+ "crossbeam-utils",
+ "globset",
+ "lazy_static",
+ "log",
+ "memchr",
+ "regex",
+ "same-file",
+ "thread_local",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b"
+dependencies = [
+ "autocfg 1.0.1",
+ "hashbrown",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "iovec"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "js-sys"
+version = "0.3.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lettre"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "338d9a248c4b3ef60c51941c678bb8f64e244c0a98f1eb71db027d1e777a5700"
+dependencies = [
+ "base64 0.10.1",
+ "bufstream",
+ "fast_chemail",
+ "hostname",
+ "log",
+ "native-tls",
+ "nom 4.2.3",
+ "serde",
+ "serde_derive",
+ "serde_json",
+]
+
+[[package]]
+name = "lettre_email"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd02480f8dcf48798e62113974d6ccca2129a51d241fa20f1ea349c8a42559d5"
+dependencies = [
+ "base64 0.10.1",
+ "email",
+ "lettre",
+ "mime",
+ "time",
+ "uuid 0.7.4",
+]
+
+[[package]]
+name = "lexical-core"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616"
+dependencies = [
+ "arrayvec",
+ "bitflags",
+ "cfg-if 0.1.10",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64d31059f22935e6c31830db5249ba2b7ecd54fd73a9909286f0a67aa55c2fbd"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
+dependencies = [
+ "cfg-if 0.1.10",
+]
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "matches"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
+
+[[package]]
+name = "memchr"
+version = "2.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mio"
+version = "0.6.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
+dependencies = [
+ "cfg-if 0.1.10",
+ "fuchsia-zircon",
+ "fuchsia-zircon-sys",
+ "iovec",
+ "kernel32-sys",
+ "libc",
+ "log",
+ "miow",
+ "net2",
+ "slab",
+ "winapi 0.2.8",
+]
+
+[[package]]
+name = "mio-uds"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0"
+dependencies = [
+ "iovec",
+ "libc",
+ "mio",
+]
+
+[[package]]
+name = "miow"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
+dependencies = [
+ "kernel32-sys",
+ "net2",
+ "winapi 0.2.8",
+ "ws2_32-sys",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "net2"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "nom"
+version = "4.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
+dependencies = [
+ "memchr",
+ "version_check 0.1.5",
+]
+
+[[package]]
+name = "nom"
+version = "6.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88034cfd6b4a0d54dd14f4a507eceee36c0b70e5a02236c4e4df571102be17f0"
+dependencies = [
+ "bitvec",
+ "lexical-core",
+ "memchr",
+ "version_check 0.9.2",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg 1.0.1",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg 1.0.1",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
+
+[[package]]
+name = "opaque-debug"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "openssl"
+version = "0.10.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70"
+dependencies = [
+ "bitflags",
+ "cfg-if 1.0.0",
+ "foreign-types",
+ "lazy_static",
+ "libc",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6"
+dependencies = [
+ "autocfg 1.0.1",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272"
+dependencies = [
+ "cfg-if 1.0.0",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "parse-zoneinfo"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
+dependencies = [
+ "regex",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "pest"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
+dependencies = [
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
+dependencies = [
+ "maplit",
+ "pest",
+ "sha-1",
+]
+
+[[package]]
+name = "pin-project"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15"
+dependencies = [
+ "pin-project-internal 0.4.27",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ccc2237c2c489783abd8c4c80e5450fc0e98644555b1364da68cc29aa151ca7"
+dependencies = [
+ "pin-project-internal 1.0.2",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check 0.9.2",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check 0.9.2",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
+[[package]]
+name = "proc-macro-nested"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
+
+[[package]]
+name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+dependencies = [
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "rdrand",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
+dependencies = [
+ "autocfg 0.1.7",
+ "libc",
+ "rand_chacha 0.1.1",
+ "rand_core 0.4.2",
+ "rand_hc 0.1.0",
+ "rand_isaac",
+ "rand_jitter",
+ "rand_os",
+ "rand_pcg",
+ "rand_xorshift",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc 0.2.0",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a76330fb486679b4ace3670f117bbc9e16204005c4bde9c4bd372f45bed34f12"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.0",
+ "rand_core 0.6.0",
+ "rand_hc 0.3.0",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
+dependencies = [
+ "autocfg 0.1.7",
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.0",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8b34ba8cfb21243bd8df91854c830ff0d785fff2e82ebd4434c2644cb9ada18"
+dependencies = [
+ "getrandom 0.2.0",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
+dependencies = [
+ "rand_core 0.6.0",
+]
+
+[[package]]
+name = "rand_isaac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_jitter"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
+dependencies = [
+ "libc",
+ "rand_core 0.4.2",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand_os"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
+dependencies = [
+ "cloudabi",
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.4.2",
+ "rdrand",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
+dependencies = [
+ "autocfg 0.1.7",
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
+
+[[package]]
+name = "regex"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rustls"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81"
+dependencies = [
+ "base64 0.12.3",
+ "log",
+ "ring",
+ "sct",
+ "webpki",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "sct"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
+dependencies = [
+ "semver-parser",
+ "serde",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e012c6c5380fb91897ba7b9261a0f565e624e869d42fe1a1d03fa0d68a083d5"
+dependencies = [
+ "pest",
+ "pest_derive",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha-1"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
+dependencies = [
+ "block-buffer 0.7.3",
+ "digest 0.8.1",
+ "fake-simd",
+ "opaque-debug 0.2.3",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if 1.0.0",
+ "cpuid-bool",
+ "digest 0.9.0",
+ "opaque-debug 0.3.0",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
+
+[[package]]
+name = "slug"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
+dependencies = [
+ "deunicode",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a55ca5f3b68e41c979bf8c46a6f1da892ca4db8f94023ce0bd32407573b1ac0"
+
+[[package]]
+name = "socket2"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "sqlformat"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74c70f0235b9925cbb106c52af1a28b5ea4885a8b851e328b8562e257a389c2d"
+dependencies = [
+ "lazy_static",
+ "maplit",
+ "nom 6.0.1",
+ "regex",
+ "unicode_categories",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1a98f9bf17b690f026b6fec565293a995b46dfbd6293debcb654dcffd2d1b34"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36bb6a2ca3345a86493bc3b71eabc2c6c16a8bb1aa476cf5303bee27f67627d7"
+dependencies = [
+ "ahash 0.6.2",
+ "atoi",
+ "bitflags",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "crc",
+ "crossbeam-channel",
+ "crossbeam-queue",
+ "crossbeam-utils",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "hashlink",
+ "hex",
+ "itoa",
+ "libc",
+ "libsqlite3-sys",
+ "log",
+ "memchr",
+ "once_cell",
+ "parking_lot",
+ "percent-encoding",
+ "rustls",
+ "sha2",
+ "smallvec",
+ "sqlformat",
+ "sqlx-rt",
+ "stringprep",
+ "thiserror",
+ "url",
+ "webpki",
+ "webpki-roots",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b5ada8b3b565331275ce913368565a273a74faf2a34da58c4dc010ce3286844"
+dependencies = [
+ "cargo_metadata",
+ "dotenv",
+ "either",
+ "futures",
+ "heck",
+ "lazy_static",
+ "proc-macro2",
+ "quote",
+ "sha2",
+ "sqlx-core",
+ "sqlx-rt",
+ "syn",
+ "url",
+]
+
+[[package]]
+name = "sqlx-rt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63fc5454c9dd7aaea3a0eeeb65ca40d06d0d8e7413a8184f7c3a3ffa5056190b"
+dependencies = [
+ "once_cell",
+ "tokio",
+ "tokio-rustls",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "stringprep"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "structopt"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c"
+dependencies = [
+ "clap",
+ "lazy_static",
+ "structopt-derive",
+]
+
+[[package]]
+name = "structopt-derive"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9802ddde94170d186eeee5005b798d9c159fa970403f1be19976d0cfb939b72"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e"
+
+[[package]]
+name = "tempfile"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "rand 0.7.3",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "tera"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac6ab7eacf40937241959d540670f06209c38ceadb62116999db4a950fbf8dc"
+dependencies = [
+ "chrono",
+ "chrono-tz",
+ "globwalk",
+ "humansize",
+ "lazy_static",
+ "percent-encoding",
+ "pest",
+ "pest_derive",
+ "rand 0.8.0",
+ "regex",
+ "serde",
+ "serde_json",
+ "slug",
+ "unic-segment",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "time"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "iovec",
+ "lazy_static",
+ "libc",
+ "memchr",
+ "mio",
+ "mio-uds",
+ "num_cpus",
+ "pin-project-lite 0.1.11",
+ "slab",
+ "tokio-macros",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a"
+dependencies = [
+ "futures-core",
+ "rustls",
+ "tokio",
+ "webpki",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite 0.1.11",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860"
+
+[[package]]
+name = "tracing"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3"
+dependencies = [
+ "cfg-if 1.0.0",
+ "log",
+ "pin-project-lite 0.2.0",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "tracing-futures"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c"
+dependencies = [
+ "pin-project 0.4.27",
+ "tracing",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "typenum"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
+dependencies = [
+ "unic-ucd-segment",
+]
+
+[[package]]
+name = "unic-ucd-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
+
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "url"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "uuid"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a"
+dependencies = [
+ "rand 0.6.5",
+]
+
+[[package]]
+name = "uuid"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"
+dependencies = [
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
+
+[[package]]
+name = "version_check"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
+
+[[package]]
+name = "walkdir"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
+dependencies = [
+ "same-file",
+ "winapi 0.3.9",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158"
+
+[[package]]
+name = "web-sys"
+version = "0.3.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376"
+dependencies = [
+ "webpki",
+]
+
+[[package]]
+name = "whoami"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d595b2e146f36183d6a590b8d41568e2bc84c922267f43baf61c956330eeb436"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "winapi"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winutil"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "ws2_32-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
+[[package]]
+name = "wyz"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..3ae6867
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "budget"
+version = "0.1.0"
+authors = ["Joris <joris@guyonvarch.me>"]
+edition = "2018"
+
+[dependencies]
+bcrypt = "0.9"
+chrono = "0.4"
+env_logger = "0.8"
+hyper = "0.13"
+lettre = "0.9"
+lettre_email = "0.9"
+log = "0.4"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+serde_urlencoded = "0.7"
+sha2 = "0.9"
+sqlx = { version = "0.4", features = ["runtime-tokio-rustls", "sqlite", "chrono"] }
+sqlx-core = "0.4"
+structopt = "0.3"
+tera = { version = "1.6", features = ["builtins"] }
+tokio = { version = "0.2", features = ["macros", "sync", "rt-threaded"] }
+tokio-util = { version = "0.3", features = ["codec"] }
+url = "2.2"
+uuid = { version = "0.8", features = ["v4"] }
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 46bbcc7..0000000
--- a/Makefile
+++ /dev/null
@@ -1,62 +0,0 @@
-start:
- @nix-shell nix/tools.nix --command "tmuxinator local"
-
-stop:
- @tmux kill-session -t sharedCost
-
-clean: clean-server clean-client
-
-build: build-server build-client cp-client
-
-# Client
-# ------
-
-clean-client:
- @rm -rf dist-client
-
-build-client:
- @nix-shell -A shells.ghcjs --run "make build-client-inside"
-
-build-client-inside:
- @cabal --project-file=cabal-client.project --builddir=dist-client new-build all
-
-cp-client:
- @cp dist-client/build/x86_64-linux/ghcjs-*/client-*/*/client/build/client/client.jsexe/all.js public/javascript/main.js
-
-watch-client:
- @nix-shell -A shells.ghcjs --run "nodemon --delay 0.2 --watch client --watch common --ext hs --exec '(tput reset && make build-client-inside && make cp-client) || true'"
-
-# Server
-# ------
-
-clean-server:
- @rm -rf dist-server
-
-build-server:
- @nix-shell -A shells.ghc --run "make build-server-inside"
-
-build-server-inside:
- @cabal --project-file=cabal-server.project --builddir=dist-server new-build all
-
-run-server:
- @(fuser -k 3000/tcp &>/dev/null) || :
- @./dist-server/build/x86_64-linux/ghc-*/server-0.0.1/*/server/build/server/server
-
-watch-server:
- @nix-shell -A shells.ghc --run "nodemon --delay 0.2 --watch ./server --watch ./common --ext hs --exec '(tput reset && make build-server-inside && make run-server) || :'"
-
-# Deploy
-# ------
-
-deploy:
- @make clean
- @nix-build -o result-server -A ghc.server
- @nix-build -o result-client -A ghcjs.client
- @nix-shell -p closurecompiler --command 'closure-compiler result-client/bin/client.jsexe/all.js --js_output_file public/javascript/main.js'
- @rm -rf bundle
- @mkdir bundle
- @cp application.conf bundle
- @cp -r public bundle
- @cp result-server/bin/server bundle
- @rsync -avzhr bundle/ guyonvarch.me:servers/shared-cost
- @rm -rf bundle
diff --git a/README.md b/README.md
index 8c736d4..b5c09a2 100644
--- a/README.md
+++ b/README.md
@@ -1,59 +1,41 @@
-# Shared Cost
+# Budget
-Share costs with a group of people:
+- pay according to your income,
+- configure monthly payments,
+- get statistics,
+- get weekly activity by email.
-- Share according to people income,
-- Monthly payments available,
-- Statistics by month,
-- Weekly activity sent by email.
+# Technologies
-## Getting started
+- database: Sqlite
+- server: Rust with hyper, sqlx,
+- templates: Tera,
+- frontend: JavaScript,
+- style: CSS.
-Install nix:
+# Screenshots
-```
-curl https://nixos.org/nix/install | sh
-```
+## Payments
-Start the environment with:
+![Payments](docs/payments.png)
-```bash
-./make start
-```
+## Balance
-Init the database with migration scripts:
+![Balance](docs/balance.png)
-```bash
-sqlite3 database < server/migrations/1.sql
-sqlite3 database < server/migrations/2.sql
-sqlite3 database < server/migrations/3.sql
-```
+## Statistics
-Inside the tmux session, add some users with sqlite after the migration is done:
+![Statistics](docs/statistics.png)
-```
-sqlite3 database
-insert into user(creation, email, name, password) values (datetime('now'), 'john@mail.com', 'John', '$2y$14$1QqyMA8vknmSVBq9BcGi6upZISLwsP2aPXx5JZOMPVzaZ8gorrsq.');
-insert into user(creation, email, name, password) values (datetime('now'), 'lisa@mail.com', 'Lisa', '$2y$14$1QqyMA8vknmSVBq9BcGi6upZISLwsP2aPXx5JZOMPVzaZ8gorrsq.');
-```
+# Getting started
-Later, stop the environment with:
+1. Use `nix-shell` to download dependencies.
-```bash
-./make stop
-```
+2. Initialize the database with `bin/db init`.
-## Deploy
+3. Start the application with `bin/watch run`.
-```bash
-make deploy
-```
+4. Connect with either:
-## Configuration
-
-See [application.conf](application.conf).
-
-## Documentation
-
-- [reflex](https://hackage.haskell.org/package/reflex-0.6.2.4/docs/doc-index-All.html)
-- [reflex-dom](https://hackage.haskell.org/package/reflex-dom-core-0.5/docs/doc-index-All.html)
+- `john@mail.com` / `password`
+- or `lisa@mail.com` / `password`.
diff --git a/application.conf b/application.conf
deleted file mode 100644
index 021fa2a..0000000
--- a/application.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-hostname = "localhost:3000"
-port = 3000
-currency = "€"
-signInExpiration = 5 minutes
-noReplyMail = "no-reply@mail.com"
-https = False
-devMode = True
-
-importMaybe "local.conf"
diff --git a/assets/chart.js b/assets/chart.js
new file mode 100644
index 0000000..7134d26
--- /dev/null
+++ b/assets/chart.js
@@ -0,0 +1,7 @@
+/*!
+ * Chart.js v2.9.4
+ * https://www.chartjs.org
+ * (c) 2020 Chart.js Contributors
+ * Released under the MIT License
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Chart=e()}(this,(function(){"use strict";"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function t(){throw new Error("Dynamic requires are not currently supported by rollup-plugin-commonjs")}function e(t,e){return t(e={exports:{}},e.exports),e.exports}var n={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]},i=e((function(t){var e={};for(var i in n)n.hasOwnProperty(i)&&(e[n[i]]=i);var a=t.exports={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};for(var r in a)if(a.hasOwnProperty(r)){if(!("channels"in a[r]))throw new Error("missing channels property: "+r);if(!("labels"in a[r]))throw new Error("missing channel labels property: "+r);if(a[r].labels.length!==a[r].channels)throw new Error("channel and label counts mismatch: "+r);var o=a[r].channels,s=a[r].labels;delete a[r].channels,delete a[r].labels,Object.defineProperty(a[r],"channels",{value:o}),Object.defineProperty(a[r],"labels",{value:s})}a.rgb.hsl=function(t){var e,n,i=t[0]/255,a=t[1]/255,r=t[2]/255,o=Math.min(i,a,r),s=Math.max(i,a,r),l=s-o;return s===o?e=0:i===s?e=(a-r)/l:a===s?e=2+(r-i)/l:r===s&&(e=4+(i-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),n=(o+s)/2,[e,100*(s===o?0:n<=.5?l/(s+o):l/(2-s-o)),100*n]},a.rgb.hsv=function(t){var e,n,i,a,r,o=t[0]/255,s=t[1]/255,l=t[2]/255,u=Math.max(o,s,l),d=u-Math.min(o,s,l),h=function(t){return(u-t)/6/d+.5};return 0===d?a=r=0:(r=d/u,e=h(o),n=h(s),i=h(l),o===u?a=i-n:s===u?a=1/3+e-i:l===u&&(a=2/3+n-e),a<0?a+=1:a>1&&(a-=1)),[360*a,100*r,100*u]},a.rgb.hwb=function(t){var e=t[0],n=t[1],i=t[2];return[a.rgb.hsl(t)[0],100*(1/255*Math.min(e,Math.min(n,i))),100*(i=1-1/255*Math.max(e,Math.max(n,i)))]},a.rgb.cmyk=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255;return[100*((1-n-(e=Math.min(1-n,1-i,1-a)))/(1-e)||0),100*((1-i-e)/(1-e)||0),100*((1-a-e)/(1-e)||0),100*e]},a.rgb.keyword=function(t){var i=e[t];if(i)return i;var a,r,o,s=1/0;for(var l in n)if(n.hasOwnProperty(l)){var u=n[l],d=(r=t,o=u,Math.pow(r[0]-o[0],2)+Math.pow(r[1]-o[1],2)+Math.pow(r[2]-o[2],2));d<s&&(s=d,a=l)}return a},a.keyword.rgb=function(t){return n[t]},a.rgb.xyz=function(t){var e=t[0]/255,n=t[1]/255,i=t[2]/255;return[100*(.4124*(e=e>.04045?Math.pow((e+.055)/1.055,2.4):e/12.92)+.3576*(n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92)+.1805*(i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92)),100*(.2126*e+.7152*n+.0722*i),100*(.0193*e+.1192*n+.9505*i)]},a.rgb.lab=function(t){var e=a.rgb.xyz(t),n=e[0],i=e[1],r=e[2];return i/=100,r/=108.883,n=(n/=95.047)>.008856?Math.pow(n,1/3):7.787*n+16/116,[116*(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116)-16,500*(n-i),200*(i-(r=r>.008856?Math.pow(r,1/3):7.787*r+16/116))]},a.hsl.rgb=function(t){var e,n,i,a,r,o=t[0]/360,s=t[1]/100,l=t[2]/100;if(0===s)return[r=255*l,r,r];e=2*l-(n=l<.5?l*(1+s):l+s-l*s),a=[0,0,0];for(var u=0;u<3;u++)(i=o+1/3*-(u-1))<0&&i++,i>1&&i--,r=6*i<1?e+6*(n-e)*i:2*i<1?n:3*i<2?e+(n-e)*(2/3-i)*6:e,a[u]=255*r;return a},a.hsl.hsv=function(t){var e=t[0],n=t[1]/100,i=t[2]/100,a=n,r=Math.max(i,.01);return n*=(i*=2)<=1?i:2-i,a*=r<=1?r:2-r,[e,100*(0===i?2*a/(r+a):2*n/(i+n)),100*((i+n)/2)]},a.hsv.rgb=function(t){var e=t[0]/60,n=t[1]/100,i=t[2]/100,a=Math.floor(e)%6,r=e-Math.floor(e),o=255*i*(1-n),s=255*i*(1-n*r),l=255*i*(1-n*(1-r));switch(i*=255,a){case 0:return[i,l,o];case 1:return[s,i,o];case 2:return[o,i,l];case 3:return[o,s,i];case 4:return[l,o,i];case 5:return[i,o,s]}},a.hsv.hsl=function(t){var e,n,i,a=t[0],r=t[1]/100,o=t[2]/100,s=Math.max(o,.01);return i=(2-r)*o,n=r*s,[a,100*(n=(n/=(e=(2-r)*s)<=1?e:2-e)||0),100*(i/=2)]},a.hwb.rgb=function(t){var e,n,i,a,r,o,s,l=t[0]/360,u=t[1]/100,d=t[2]/100,h=u+d;switch(h>1&&(u/=h,d/=h),i=6*l-(e=Math.floor(6*l)),0!=(1&e)&&(i=1-i),a=u+i*((n=1-d)-u),e){default:case 6:case 0:r=n,o=a,s=u;break;case 1:r=a,o=n,s=u;break;case 2:r=u,o=n,s=a;break;case 3:r=u,o=a,s=n;break;case 4:r=a,o=u,s=n;break;case 5:r=n,o=u,s=a}return[255*r,255*o,255*s]},a.cmyk.rgb=function(t){var e=t[0]/100,n=t[1]/100,i=t[2]/100,a=t[3]/100;return[255*(1-Math.min(1,e*(1-a)+a)),255*(1-Math.min(1,n*(1-a)+a)),255*(1-Math.min(1,i*(1-a)+a))]},a.xyz.rgb=function(t){var e,n,i,a=t[0]/100,r=t[1]/100,o=t[2]/100;return n=-.9689*a+1.8758*r+.0415*o,i=.0557*a+-.204*r+1.057*o,e=(e=3.2406*a+-1.5372*r+-.4986*o)>.0031308?1.055*Math.pow(e,1/2.4)-.055:12.92*e,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:12.92*n,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:12.92*i,[255*(e=Math.min(Math.max(0,e),1)),255*(n=Math.min(Math.max(0,n),1)),255*(i=Math.min(Math.max(0,i),1))]},a.xyz.lab=function(t){var e=t[0],n=t[1],i=t[2];return n/=100,i/=108.883,e=(e/=95.047)>.008856?Math.pow(e,1/3):7.787*e+16/116,[116*(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116)-16,500*(e-n),200*(n-(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116))]},a.lab.xyz=function(t){var e,n,i,a=t[0];e=t[1]/500+(n=(a+16)/116),i=n-t[2]/200;var r=Math.pow(n,3),o=Math.pow(e,3),s=Math.pow(i,3);return n=r>.008856?r:(n-16/116)/7.787,e=o>.008856?o:(e-16/116)/7.787,i=s>.008856?s:(i-16/116)/7.787,[e*=95.047,n*=100,i*=108.883]},a.lab.lch=function(t){var e,n=t[0],i=t[1],a=t[2];return(e=360*Math.atan2(a,i)/2/Math.PI)<0&&(e+=360),[n,Math.sqrt(i*i+a*a),e]},a.lch.lab=function(t){var e,n=t[0],i=t[1];return e=t[2]/360*2*Math.PI,[n,i*Math.cos(e),i*Math.sin(e)]},a.rgb.ansi16=function(t){var e=t[0],n=t[1],i=t[2],r=1 in arguments?arguments[1]:a.rgb.hsv(t)[2];if(0===(r=Math.round(r/50)))return 30;var o=30+(Math.round(i/255)<<2|Math.round(n/255)<<1|Math.round(e/255));return 2===r&&(o+=60),o},a.hsv.ansi16=function(t){return a.rgb.ansi16(a.hsv.rgb(t),t[2])},a.rgb.ansi256=function(t){var e=t[0],n=t[1],i=t[2];return e===n&&n===i?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(n/255*5)+Math.round(i/255*5)},a.ansi16.rgb=function(t){var e=t%10;if(0===e||7===e)return t>50&&(e+=3.5),[e=e/10.5*255,e,e];var n=.5*(1+~~(t>50));return[(1&e)*n*255,(e>>1&1)*n*255,(e>>2&1)*n*255]},a.ansi256.rgb=function(t){if(t>=232){var e=10*(t-232)+8;return[e,e,e]}var n;return t-=16,[Math.floor(t/36)/5*255,Math.floor((n=t%36)/6)/5*255,n%6/5*255]},a.rgb.hex=function(t){var e=(((255&Math.round(t[0]))<<16)+((255&Math.round(t[1]))<<8)+(255&Math.round(t[2]))).toString(16).toUpperCase();return"000000".substring(e.length)+e},a.hex.rgb=function(t){var e=t.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!e)return[0,0,0];var n=e[0];3===e[0].length&&(n=n.split("").map((function(t){return t+t})).join(""));var i=parseInt(n,16);return[i>>16&255,i>>8&255,255&i]},a.rgb.hcg=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255,r=Math.max(Math.max(n,i),a),o=Math.min(Math.min(n,i),a),s=r-o;return e=s<=0?0:r===n?(i-a)/s%6:r===i?2+(a-n)/s:4+(n-i)/s+4,e/=6,[360*(e%=1),100*s,100*(s<1?o/(1-s):0)]},a.hsl.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=1,a=0;return(i=n<.5?2*e*n:2*e*(1-n))<1&&(a=(n-.5*i)/(1-i)),[t[0],100*i,100*a]},a.hsv.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=e*n,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.hcg.rgb=function(t){var e=t[0]/360,n=t[1]/100,i=t[2]/100;if(0===n)return[255*i,255*i,255*i];var a,r=[0,0,0],o=e%1*6,s=o%1,l=1-s;switch(Math.floor(o)){case 0:r[0]=1,r[1]=s,r[2]=0;break;case 1:r[0]=l,r[1]=1,r[2]=0;break;case 2:r[0]=0,r[1]=1,r[2]=s;break;case 3:r[0]=0,r[1]=l,r[2]=1;break;case 4:r[0]=s,r[1]=0,r[2]=1;break;default:r[0]=1,r[1]=0,r[2]=l}return a=(1-n)*i,[255*(n*r[0]+a),255*(n*r[1]+a),255*(n*r[2]+a)]},a.hcg.hsv=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e),i=0;return n>0&&(i=e/n),[t[0],100*i,100*n]},a.hcg.hsl=function(t){var e=t[1]/100,n=t[2]/100*(1-e)+.5*e,i=0;return n>0&&n<.5?i=e/(2*n):n>=.5&&n<1&&(i=e/(2*(1-n))),[t[0],100*i,100*n]},a.hcg.hwb=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e);return[t[0],100*(n-e),100*(1-n)]},a.hwb.hcg=function(t){var e=t[1]/100,n=1-t[2]/100,i=n-e,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.apple.rgb=function(t){return[t[0]/65535*255,t[1]/65535*255,t[2]/65535*255]},a.rgb.apple=function(t){return[t[0]/255*65535,t[1]/255*65535,t[2]/255*65535]},a.gray.rgb=function(t){return[t[0]/100*255,t[0]/100*255,t[0]/100*255]},a.gray.hsl=a.gray.hsv=function(t){return[0,0,t[0]]},a.gray.hwb=function(t){return[0,100,t[0]]},a.gray.cmyk=function(t){return[0,0,0,t[0]]},a.gray.lab=function(t){return[t[0],0,0]},a.gray.hex=function(t){var e=255&Math.round(t[0]/100*255),n=((e<<16)+(e<<8)+e).toString(16).toUpperCase();return"000000".substring(n.length)+n},a.rgb.gray=function(t){return[(t[0]+t[1]+t[2])/3/255*100]}}));i.rgb,i.hsl,i.hsv,i.hwb,i.cmyk,i.xyz,i.lab,i.lch,i.hex,i.keyword,i.ansi16,i.ansi256,i.hcg,i.apple,i.gray;function a(t){var e=function(){for(var t={},e=Object.keys(i),n=e.length,a=0;a<n;a++)t[e[a]]={distance:-1,parent:null};return t}(),n=[t];for(e[t].distance=0;n.length;)for(var a=n.pop(),r=Object.keys(i[a]),o=r.length,s=0;s<o;s++){var l=r[s],u=e[l];-1===u.distance&&(u.distance=e[a].distance+1,u.parent=a,n.unshift(l))}return e}function r(t,e){return function(n){return e(t(n))}}function o(t,e){for(var n=[e[t].parent,t],a=i[e[t].parent][t],o=e[t].parent;e[o].parent;)n.unshift(e[o].parent),a=r(i[e[o].parent][o],a),o=e[o].parent;return a.conversion=n,a}var s={};Object.keys(i).forEach((function(t){s[t]={},Object.defineProperty(s[t],"channels",{value:i[t].channels}),Object.defineProperty(s[t],"labels",{value:i[t].labels});var e=function(t){for(var e=a(t),n={},i=Object.keys(e),r=i.length,s=0;s<r;s++){var l=i[s];null!==e[l].parent&&(n[l]=o(l,e))}return n}(t);Object.keys(e).forEach((function(n){var i=e[n];s[t][n]=function(t){var e=function(e){if(null==e)return e;arguments.length>1&&(e=Array.prototype.slice.call(arguments));var n=t(e);if("object"==typeof n)for(var i=n.length,a=0;a<i;a++)n[a]=Math.round(n[a]);return n};return"conversion"in t&&(e.conversion=t.conversion),e}(i),s[t][n].raw=function(t){var e=function(e){return null==e?e:(arguments.length>1&&(e=Array.prototype.slice.call(arguments)),t(e))};return"conversion"in t&&(e.conversion=t.conversion),e}(i)}))}));var l=s,u={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]},d={getRgba:h,getHsla:c,getRgb:function(t){var e=h(t);return e&&e.slice(0,3)},getHsl:function(t){var e=c(t);return e&&e.slice(0,3)},getHwb:f,getAlpha:function(t){var e=h(t);if(e)return e[3];if(e=c(t))return e[3];if(e=f(t))return e[3]},hexString:function(t,e){e=void 0!==e&&3===t.length?e:t[3];return"#"+b(t[0])+b(t[1])+b(t[2])+(e>=0&&e<1?b(Math.round(255*e)):"")},rgbString:function(t,e){if(e<1||t[3]&&t[3]<1)return g(t,e);return"rgb("+t[0]+", "+t[1]+", "+t[2]+")"},rgbaString:g,percentString:function(t,e){if(e<1||t[3]&&t[3]<1)return m(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+"%)"},percentaString:m,hslString:function(t,e){if(e<1||t[3]&&t[3]<1)return p(t,e);return"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"},hslaString:p,hwbString:function(t,e){void 0===e&&(e=void 0!==t[3]?t[3]:1);return"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"},keyword:function(t){return y[t.slice(0,3)]}};function h(t){if(t){var e=[0,0,0],n=1,i=t.match(/^#([a-fA-F0-9]{3,4})$/i),a="";if(i){a=(i=i[1])[3];for(var r=0;r<e.length;r++)e[r]=parseInt(i[r]+i[r],16);a&&(n=Math.round(parseInt(a+a,16)/255*100)/100)}else if(i=t.match(/^#([a-fA-F0-9]{6}([a-fA-F0-9]{2})?)$/i)){a=i[2],i=i[1];for(r=0;r<e.length;r++)e[r]=parseInt(i.slice(2*r,2*r+2),16);a&&(n=Math.round(parseInt(a,16)/255*100)/100)}else if(i=t.match(/^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)){for(r=0;r<e.length;r++)e[r]=parseInt(i[r+1]);n=parseFloat(i[4])}else if(i=t.match(/^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i)){for(r=0;r<e.length;r++)e[r]=Math.round(2.55*parseFloat(i[r+1]));n=parseFloat(i[4])}else if(i=t.match(/(\w+)/)){if("transparent"==i[1])return[0,0,0,0];if(!(e=u[i[1]]))return}for(r=0;r<e.length;r++)e[r]=v(e[r],0,255);return n=n||0==n?v(n,0,1):1,e[3]=n,e}}function c(t){if(t){var e=t.match(/^hsla?\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/);if(e){var n=parseFloat(e[4]);return[v(parseInt(e[1]),0,360),v(parseFloat(e[2]),0,100),v(parseFloat(e[3]),0,100),v(isNaN(n)?1:n,0,1)]}}}function f(t){if(t){var e=t.match(/^hwb\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/);if(e){var n=parseFloat(e[4]);return[v(parseInt(e[1]),0,360),v(parseFloat(e[2]),0,100),v(parseFloat(e[3]),0,100),v(isNaN(n)?1:n,0,1)]}}}function g(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"rgba("+t[0]+", "+t[1]+", "+t[2]+", "+e+")"}function m(t,e){return"rgba("+Math.round(t[0]/255*100)+"%, "+Math.round(t[1]/255*100)+"%, "+Math.round(t[2]/255*100)+"%, "+(e||t[3]||1)+")"}function p(t,e){return void 0===e&&(e=void 0!==t[3]?t[3]:1),"hsla("+t[0]+", "+t[1]+"%, "+t[2]+"%, "+e+")"}function v(t,e,n){return Math.min(Math.max(e,t),n)}function b(t){var e=t.toString(16).toUpperCase();return e.length<2?"0"+e:e}var y={};for(var x in u)y[u[x]]=x;var _=function(t){return t instanceof _?t:this instanceof _?(this.valid=!1,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},void("string"==typeof t?(e=d.getRgba(t))?this.setValues("rgb",e):(e=d.getHsla(t))?this.setValues("hsl",e):(e=d.getHwb(t))&&this.setValues("hwb",e):"object"==typeof t&&(void 0!==(e=t).r||void 0!==e.red?this.setValues("rgb",e):void 0!==e.l||void 0!==e.lightness?this.setValues("hsl",e):void 0!==e.v||void 0!==e.value?this.setValues("hsv",e):void 0!==e.w||void 0!==e.whiteness?this.setValues("hwb",e):void 0===e.c&&void 0===e.cyan||this.setValues("cmyk",e)))):new _(t);var e};_.prototype={isValid:function(){return this.valid},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=(t%=360)<0?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 d.hexString(this.values.rgb)},rgbString:function(){return d.rgbString(this.values.rgb,this.values.alpha)},rgbaString:function(){return d.rgbaString(this.values.rgb,this.values.alpha)},percentString:function(){return d.percentString(this.values.rgb,this.values.alpha)},hslString:function(){return d.hslString(this.values.hsl,this.values.alpha)},hslaString:function(){return d.hslaString(this.values.hsl,this.values.alpha)},hwbString:function(){return d.hwbString(this.values.hwb,this.values.alpha)},keyword:function(){return d.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<t.length;n++){var i=t[n]/255;e[n]=i<=.03928?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;return(299*t[0]+587*t[1]+114*t[2])/1e3<128},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;e<3;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){var e=this.values.hsl;return e[2]+=e[2]*t,this.setValues("hsl",e),this},darken:function(t){var e=this.values.hsl;return e[2]-=e[2]*t,this.setValues("hsl",e),this},saturate:function(t){var e=this.values.hsl;return e[1]+=e[1]*t,this.setValues("hsl",e),this},desaturate:function(t){var e=this.values.hsl;return e[1]-=e[1]*t,this.setValues("hsl",e),this},whiten:function(t){var e=this.values.hwb;return e[1]+=e[1]*t,this.setValues("hwb",e),this},blacken:function(t){var e=this.values.hwb;return e[2]+=e[2]*t,this.setValues("hwb",e),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){var e=this.values.alpha;return this.setValues("alpha",e-e*t),this},opaquer:function(t){var e=this.values.alpha;return this.setValues("alpha",e+e*t),this},rotate:function(t){var e=this.values.hsl,n=(e[0]+t)%360;return e[0]=n<0?360+n:n,this.setValues("hsl",e),this},mix:function(t,e){var n=t,i=void 0===e?.5:e,a=2*i-1,r=this.alpha()-n.alpha(),o=((a*r==-1?a:(a+r)/(1+a*r))+1)/2,s=1-o;return this.rgb(o*this.red()+s*n.red(),o*this.green()+s*n.green(),o*this.blue()+s*n.blue()).alpha(this.alpha()*i+n.alpha()*(1-i))},toJSON:function(){return this.rgb()},clone:function(){var t,e,n=new _,i=this.values,a=n.values;for(var r in i)i.hasOwnProperty(r)&&(t=i[r],"[object Array]"===(e={}.toString.call(t))?a[r]=t.slice(0):"[object Number]"===e?a[r]=t:console.error("unexpected color value:",t));return n}},_.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},_.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},_.prototype.getValues=function(t){for(var e=this.values,n={},i=0;i<t.length;i++)n[t.charAt(i)]=e[t][i];return 1!==e.alpha&&(n.a=e.alpha),n},_.prototype.setValues=function(t,e){var n,i,a=this.values,r=this.spaces,o=this.maxes,s=1;if(this.valid=!0,"alpha"===t)s=e;else if(e.length)a[t]=e.slice(0,t.length),s=e[t.length];else if(void 0!==e[t.charAt(0)]){for(n=0;n<t.length;n++)a[t][n]=e[t.charAt(n)];s=e.a}else if(void 0!==e[r[t][0]]){var u=r[t];for(n=0;n<t.length;n++)a[t][n]=e[u[n]];s=e.alpha}if(a.alpha=Math.max(0,Math.min(1,void 0===s?a.alpha:s)),"alpha"===t)return!1;for(n=0;n<t.length;n++)i=Math.max(0,Math.min(o[t][n],a[t][n])),a[t][n]=Math.round(i);for(var d in r)d!==t&&(a[d]=l[t][d](a[t]));return!0},_.prototype.setSpace=function(t,e){var n=e[0];return void 0===n?this.getValues(t):("number"==typeof n&&(n=Array.prototype.slice.call(e)),this.setValues(t,n),this)},_.prototype.setChannel=function(t,e,n){var i=this.values[t];return void 0===n?i[e]:n===i[e]?this:(i[e]=n,this.setValues(t,i),this)},"undefined"!=typeof window&&(window.Color=_);var w=_;function k(t){return-1===["__proto__","prototype","constructor"].indexOf(t)}var M,S={noop:function(){},uid:(M=0,function(){return M++}),isNullOrUndef:function(t){return null==t},isArray:function(t){if(Array.isArray&&Array.isArray(t))return!0;var e=Object.prototype.toString.call(t);return"[object"===e.substr(0,7)&&"Array]"===e.substr(-6)},isObject:function(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)},isFinite:function(t){return("number"==typeof t||t instanceof Number)&&isFinite(t)},valueOrDefault:function(t,e){return void 0===t?e:t},valueAtIndexOrDefault:function(t,e,n){return S.valueOrDefault(S.isArray(t)?t[e]:t,n)},callback:function(t,e,n){if(t&&"function"==typeof t.call)return t.apply(n,e)},each:function(t,e,n,i){var a,r,o;if(S.isArray(t))if(r=t.length,i)for(a=r-1;a>=0;a--)e.call(n,t[a],a);else for(a=0;a<r;a++)e.call(n,t[a],a);else if(S.isObject(t))for(r=(o=Object.keys(t)).length,a=0;a<r;a++)e.call(n,t[o[a]],o[a])},arrayEquals:function(t,e){var n,i,a,r;if(!t||!e||t.length!==e.length)return!1;for(n=0,i=t.length;n<i;++n)if(a=t[n],r=e[n],a instanceof Array&&r instanceof Array){if(!S.arrayEquals(a,r))return!1}else if(a!==r)return!1;return!0},clone:function(t){if(S.isArray(t))return t.map(S.clone);if(S.isObject(t)){for(var e=Object.create(t),n=Object.keys(t),i=n.length,a=0;a<i;++a)e[n[a]]=S.clone(t[n[a]]);return e}return t},_merger:function(t,e,n,i){if(k(t)){var a=e[t],r=n[t];S.isObject(a)&&S.isObject(r)?S.merge(a,r,i):e[t]=S.clone(r)}},_mergerIf:function(t,e,n){if(k(t)){var i=e[t],a=n[t];S.isObject(i)&&S.isObject(a)?S.mergeIf(i,a):e.hasOwnProperty(t)||(e[t]=S.clone(a))}},merge:function(t,e,n){var i,a,r,o,s,l=S.isArray(e)?e:[e],u=l.length;if(!S.isObject(t))return t;for(i=(n=n||{}).merger||S._merger,a=0;a<u;++a)if(e=l[a],S.isObject(e))for(s=0,o=(r=Object.keys(e)).length;s<o;++s)i(r[s],t,e,n);return t},mergeIf:function(t,e){return S.merge(t,e,{merger:S._mergerIf})},extend:Object.assign||function(t){return S.merge(t,[].slice.call(arguments,1),{merger:function(t,e,n){e[t]=n[t]}})},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=S.inherits,t&&S.extend(n.prototype,t),n.__super__=e.prototype,n},_deprecated:function(t,e,n,i){void 0!==e&&console.warn(t+': "'+n+'" is deprecated. Please use "'+i+'" instead')}},D=S;S.callCallback=S.callback,S.indexOf=function(t,e,n){return Array.prototype.indexOf.call(t,e,n)},S.getValueOrDefault=S.valueOrDefault,S.getValueAtIndexOrDefault=S.valueAtIndexOrDefault;var C={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return(t-=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-((t-=1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return t*t*t*t*t},easeOutQuint:function(t){return(t-=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*(Math.PI/2))},easeOutSine:function(t){return Math.sin(t*(Math.PI/2))},easeInOutSine:function(t){return-.5*(Math.cos(Math.PI*t)-1)},easeInExpo:function(t){return 0===t?0:Math.pow(2,10*(t-1))},easeOutExpo:function(t){return 1===t?1:1-Math.pow(2,-10*t)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(2-Math.pow(2,-10*--t))},easeInCirc:function(t){return t>=1?t:-(Math.sqrt(1-t*t)-1)},easeOutCirc:function(t){return Math.sqrt(1-(t-=1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===t?1:(n||(n=.3),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),-i*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n))},easeOutElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===t?1:(n||(n=.3),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),i*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/n)+1)},easeInOutElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:2==(t/=.5)?1:(n||(n=.45),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),t<1?i*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n)*-.5:i*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n)*.5+1)},easeInBack:function(t){var e=1.70158;return t*t*((e+1)*t-e)},easeOutBack:function(t){var e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:function(t){return 1-C.easeOutBounce(1-t)},easeOutBounce:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},easeInOutBounce:function(t){return t<.5?.5*C.easeInBounce(2*t):.5*C.easeOutBounce(2*t-1)+.5}},P={effects:C};D.easingEffects=C;var T=Math.PI,O=T/180,A=2*T,F=T/2,I=T/4,L=2*T/3,R={clear:function(t){t.ctx.clearRect(0,0,t.width,t.height)},roundedRect:function(t,e,n,i,a,r){if(r){var o=Math.min(r,a/2,i/2),s=e+o,l=n+o,u=e+i-o,d=n+a-o;t.moveTo(e,l),s<u&&l<d?(t.arc(s,l,o,-T,-F),t.arc(u,l,o,-F,0),t.arc(u,d,o,0,F),t.arc(s,d,o,F,T)):s<u?(t.moveTo(s,n),t.arc(u,l,o,-F,F),t.arc(s,l,o,F,T+F)):l<d?(t.arc(s,l,o,-T,0),t.arc(s,d,o,0,T)):t.arc(s,l,o,-T,T),t.closePath(),t.moveTo(e,n)}else t.rect(e,n,i,a)},drawPoint:function(t,e,n,i,a,r){var o,s,l,u,d,h=(r||0)*O;if(e&&"object"==typeof e&&("[object HTMLImageElement]"===(o=e.toString())||"[object HTMLCanvasElement]"===o))return t.save(),t.translate(i,a),t.rotate(h),t.drawImage(e,-e.width/2,-e.height/2,e.width,e.height),void t.restore();if(!(isNaN(n)||n<=0)){switch(t.beginPath(),e){default:t.arc(i,a,n,0,A),t.closePath();break;case"triangle":t.moveTo(i+Math.sin(h)*n,a-Math.cos(h)*n),h+=L,t.lineTo(i+Math.sin(h)*n,a-Math.cos(h)*n),h+=L,t.lineTo(i+Math.sin(h)*n,a-Math.cos(h)*n),t.closePath();break;case"rectRounded":u=n-(d=.516*n),s=Math.cos(h+I)*u,l=Math.sin(h+I)*u,t.arc(i-s,a-l,d,h-T,h-F),t.arc(i+l,a-s,d,h-F,h),t.arc(i+s,a+l,d,h,h+F),t.arc(i-l,a+s,d,h+F,h+T),t.closePath();break;case"rect":if(!r){u=Math.SQRT1_2*n,t.rect(i-u,a-u,2*u,2*u);break}h+=I;case"rectRot":s=Math.cos(h)*n,l=Math.sin(h)*n,t.moveTo(i-s,a-l),t.lineTo(i+l,a-s),t.lineTo(i+s,a+l),t.lineTo(i-l,a+s),t.closePath();break;case"crossRot":h+=I;case"cross":s=Math.cos(h)*n,l=Math.sin(h)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l),t.moveTo(i+l,a-s),t.lineTo(i-l,a+s);break;case"star":s=Math.cos(h)*n,l=Math.sin(h)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l),t.moveTo(i+l,a-s),t.lineTo(i-l,a+s),h+=I,s=Math.cos(h)*n,l=Math.sin(h)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l),t.moveTo(i+l,a-s),t.lineTo(i-l,a+s);break;case"line":s=Math.cos(h)*n,l=Math.sin(h)*n,t.moveTo(i-s,a-l),t.lineTo(i+s,a+l);break;case"dash":t.moveTo(i,a),t.lineTo(i+Math.cos(h)*n,a+Math.sin(h)*n)}t.fill(),t.stroke()}},_isPointInArea:function(t,e){return t.x>e.left-1e-6&&t.x<e.right+1e-6&&t.y>e.top-1e-6&&t.y<e.bottom+1e-6},clipArea:function(t,e){t.save(),t.beginPath(),t.rect(e.left,e.top,e.right-e.left,e.bottom-e.top),t.clip()},unclipArea:function(t){t.restore()},lineTo:function(t,e,n,i){var a=n.steppedLine;if(a){if("middle"===a){var r=(e.x+n.x)/2;t.lineTo(r,i?n.y:e.y),t.lineTo(r,i?e.y:n.y)}else"after"===a&&!i||"after"!==a&&i?t.lineTo(e.x,n.y):t.lineTo(n.x,e.y);t.lineTo(n.x,n.y)}else n.tension?t.bezierCurveTo(i?e.controlPointPreviousX:e.controlPointNextX,i?e.controlPointPreviousY:e.controlPointNextY,i?n.controlPointNextX:n.controlPointPreviousX,i?n.controlPointNextY:n.controlPointPreviousY,n.x,n.y):t.lineTo(n.x,n.y)}},N=R;D.clear=R.clear,D.drawRoundedRectangle=function(t){t.beginPath(),R.roundedRect.apply(R,arguments)};var W={_set:function(t,e){return D.merge(this[t]||(this[t]={}),e)}};W._set("global",{defaultColor:"rgba(0,0,0,0.1)",defaultFontColor:"#666",defaultFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",defaultFontSize:12,defaultFontStyle:"normal",defaultLineHeight:1.2,showLines:!0});var Y=W,z=D.valueOrDefault;var E={toLineHeight:function(t,e){var n=(""+t).match(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/);if(!n||"normal"===n[1])return 1.2*e;switch(t=+n[2],n[3]){case"px":return t;case"%":t/=100}return e*t},toPadding:function(t){var e,n,i,a;return D.isObject(t)?(e=+t.top||0,n=+t.right||0,i=+t.bottom||0,a=+t.left||0):e=n=i=a=+t||0,{top:e,right:n,bottom:i,left:a,height:e+i,width:a+n}},_parseFont:function(t){var e=Y.global,n=z(t.fontSize,e.defaultFontSize),i={family:z(t.fontFamily,e.defaultFontFamily),lineHeight:D.options.toLineHeight(z(t.lineHeight,e.defaultLineHeight),n),size:n,style:z(t.fontStyle,e.defaultFontStyle),weight:null,string:""};return i.string=function(t){return!t||D.isNullOrUndef(t.size)||D.isNullOrUndef(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}(i),i},resolve:function(t,e,n,i){var a,r,o,s=!0;for(a=0,r=t.length;a<r;++a)if(void 0!==(o=t[a])&&(void 0!==e&&"function"==typeof o&&(o=o(e),s=!1),void 0!==n&&D.isArray(o)&&(o=o[n],s=!1),void 0!==o))return i&&!s&&(i.cacheable=!1),o}},V={_factorize:function(t){var e,n=[],i=Math.sqrt(t);for(e=1;e<i;e++)t%e==0&&(n.push(e),n.push(t/e));return i===(0|i)&&n.push(i),n.sort((function(t,e){return t-e})).pop(),n},log10:Math.log10||function(t){var e=Math.log(t)*Math.LOG10E,n=Math.round(e);return t===Math.pow(10,n)?n:e}},H=V;D.log10=V.log10;var B=D,j=P,U=N,G=E,q=H,Z={getRtlAdapter:function(t,e,n){return t?function(t,e){return{x:function(n){return t+t+e-n},setWidth:function(t){e=t},textAlign:function(t){return"center"===t?t:"right"===t?"left":"right"},xPlus:function(t,e){return t-e},leftForLtr:function(t,e){return t-e}}}(e,n):{x:function(t){return t},setWidth:function(t){},textAlign:function(t){return t},xPlus:function(t,e){return t+e},leftForLtr:function(t,e){return t}}},overrideTextDirection:function(t,e){var n,i;"ltr"!==e&&"rtl"!==e||(i=[(n=t.canvas.style).getPropertyValue("direction"),n.getPropertyPriority("direction")],n.setProperty("direction",e,"important"),t.prevTextDirection=i)},restoreTextDirection:function(t){var e=t.prevTextDirection;void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}};B.easing=j,B.canvas=U,B.options=G,B.math=q,B.rtl=Z;var $=function(t){B.extend(this,t),this.initialize.apply(this,arguments)};B.extend($.prototype,{_type:void 0,initialize:function(){this.hidden=!1},pivot:function(){var t=this;return t._view||(t._view=B.extend({},t._model)),t._start={},t},transition:function(t){var e=this,n=e._model,i=e._start,a=e._view;return n&&1!==t?(a||(a=e._view={}),i||(i=e._start={}),function(t,e,n,i){var a,r,o,s,l,u,d,h,c,f=Object.keys(n);for(a=0,r=f.length;a<r;++a)if(u=n[o=f[a]],e.hasOwnProperty(o)||(e[o]=u),(s=e[o])!==u&&"_"!==o[0]){if(t.hasOwnProperty(o)||(t[o]=s),(d=typeof u)===typeof(l=t[o]))if("string"===d){if((h=w(l)).valid&&(c=w(u)).valid){e[o]=c.mix(h,i).rgbString();continue}}else if(B.isFinite(l)&&B.isFinite(u)){e[o]=l+(u-l)*i;continue}e[o]=u}}(i,a,n,t),e):(e._view=B.extend({},n),e._start=null,e)},tooltipPosition:function(){return{x:this._model.x,y:this._model.y}},hasValue:function(){return B.isNumber(this._model.x)&&B.isNumber(this._model.y)}}),$.extend=B.inherits;var X=$,K=X.extend({chart:null,currentStep:0,numSteps:60,easing:"",render:null,onAnimationProgress:null,onAnimationComplete:null}),J=K;Object.defineProperty(K.prototype,"animationObject",{get:function(){return this}}),Object.defineProperty(K.prototype,"chartInstance",{get:function(){return this.chart},set:function(t){this.chart=t}}),Y._set("global",{animation:{duration:1e3,easing:"easeOutQuart",onProgress:B.noop,onComplete:B.noop}});var Q={animations:[],request:null,addAnimation:function(t,e,n,i){var a,r,o=this.animations;for(e.chart=t,e.startTime=Date.now(),e.duration=n,i||(t.animating=!0),a=0,r=o.length;a<r;++a)if(o[a].chart===t)return void(o[a]=e);o.push(e),1===o.length&&this.requestAnimationFrame()},cancelAnimation:function(t){var e=B.findIndex(this.animations,(function(e){return e.chart===t}));-1!==e&&(this.animations.splice(e,1),t.animating=!1)},requestAnimationFrame:function(){var t=this;null===t.request&&(t.request=B.requestAnimFrame.call(window,(function(){t.request=null,t.startDigest()})))},startDigest:function(){this.advance(),this.animations.length>0&&this.requestAnimationFrame()},advance:function(){for(var t,e,n,i,a=this.animations,r=0;r<a.length;)e=(t=a[r]).chart,n=t.numSteps,i=Math.floor((Date.now()-t.startTime)/t.duration*n)+1,t.currentStep=Math.min(i,n),B.callback(t.render,[e,t],e),B.callback(t.onAnimationProgress,[t],e),t.currentStep>=n?(B.callback(t.onAnimationComplete,[t],e),e.animating=!1,a.splice(r,1)):++r}},tt=B.options.resolve,et=["push","pop","shift","splice","unshift"];function nt(t,e){var n=t._chartjs;if(n){var i=n.listeners,a=i.indexOf(e);-1!==a&&i.splice(a,1),i.length>0||(et.forEach((function(e){delete t[e]})),delete t._chartjs)}}var it=function(t,e){this.initialize(t,e)};B.extend(it.prototype,{datasetElementType:null,dataElementType:null,_datasetElementOptions:["backgroundColor","borderCapStyle","borderColor","borderDash","borderDashOffset","borderJoinStyle","borderWidth"],_dataElementOptions:["backgroundColor","borderColor","borderWidth","pointStyle"],initialize:function(t,e){var n=this;n.chart=t,n.index=e,n.linkScales(),n.addElements(),n._type=n.getMeta().type},updateIndex:function(t){this.index=t},linkScales:function(){var t=this.getMeta(),e=this.chart,n=e.scales,i=this.getDataset(),a=e.options.scales;null!==t.xAxisID&&t.xAxisID in n&&!i.xAxisID||(t.xAxisID=i.xAxisID||a.xAxes[0].id),null!==t.yAxisID&&t.yAxisID in n&&!i.yAxisID||(t.yAxisID=i.yAxisID||a.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]},_getValueScaleId:function(){return this.getMeta().yAxisID},_getIndexScaleId:function(){return this.getMeta().xAxisID},_getValueScale:function(){return this.getScaleForId(this._getValueScaleId())},_getIndexScale:function(){return this.getScaleForId(this._getIndexScaleId())},reset:function(){this._update(!0)},destroy:function(){this._data&&nt(this._data,this)},createMetaDataset:function(){var t=this.datasetElementType;return t&&new t({_chart:this.chart,_datasetIndex:this.index})},createMetaData:function(t){var e=this.dataElementType;return e&&new e({_chart:this.chart,_datasetIndex:this.index,_index:t})},addElements:function(){var t,e,n=this.getMeta(),i=this.getDataset().data||[],a=n.data;for(t=0,e=i.length;t<e;++t)a[t]=a[t]||this.createMetaData(t);n.dataset=n.dataset||this.createMetaDataset()},addElementAndReset:function(t){var e=this.createMetaData(t);this.getMeta().data.splice(t,0,e),this.updateElement(e,t,!0)},buildOrUpdateElements:function(){var t,e,n=this,i=n.getDataset(),a=i.data||(i.data=[]);n._data!==a&&(n._data&&nt(n._data,n),a&&Object.isExtensible(a)&&(e=n,(t=a)._chartjs?t._chartjs.listeners.push(e):(Object.defineProperty(t,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[e]}}),et.forEach((function(e){var n="onData"+e.charAt(0).toUpperCase()+e.slice(1),i=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value:function(){var e=Array.prototype.slice.call(arguments),a=i.apply(this,e);return B.each(t._chartjs.listeners,(function(t){"function"==typeof t[n]&&t[n].apply(t,e)})),a}})})))),n._data=a),n.resyncElements()},_configure:function(){this._config=B.merge(Object.create(null),[this.chart.options.datasets[this._type],this.getDataset()],{merger:function(t,e,n){"_meta"!==t&&"data"!==t&&B._merger(t,e,n)}})},_update:function(t){this._configure(),this._cachedDataOpts=null,this.update(t)},update:B.noop,transition:function(t){for(var e=this.getMeta(),n=e.data||[],i=n.length,a=0;a<i;++a)n[a].transition(t);e.dataset&&e.dataset.transition(t)},draw:function(){var t=this.getMeta(),e=t.data||[],n=e.length,i=0;for(t.dataset&&t.dataset.draw();i<n;++i)e[i].draw()},getStyle:function(t){var e,n=this.getMeta(),i=n.dataset;return this._configure(),i&&void 0===t?e=this._resolveDatasetElementOptions(i||{}):(t=t||0,e=this._resolveDataElementOptions(n.data[t]||{},t)),!1!==e.fill&&null!==e.fill||(e.backgroundColor=e.borderColor),e},_resolveDatasetElementOptions:function(t,e){var n,i,a,r,o=this,s=o.chart,l=o._config,u=t.custom||{},d=s.options.elements[o.datasetElementType.prototype._type]||{},h=o._datasetElementOptions,c={},f={chart:s,dataset:o.getDataset(),datasetIndex:o.index,hover:e};for(n=0,i=h.length;n<i;++n)a=h[n],r=e?"hover"+a.charAt(0).toUpperCase()+a.slice(1):a,c[a]=tt([u[r],l[r],d[r]],f);return c},_resolveDataElementOptions:function(t,e){var n=this,i=t&&t.custom,a=n._cachedDataOpts;if(a&&!i)return a;var r,o,s,l,u=n.chart,d=n._config,h=u.options.elements[n.dataElementType.prototype._type]||{},c=n._dataElementOptions,f={},g={chart:u,dataIndex:e,dataset:n.getDataset(),datasetIndex:n.index},m={cacheable:!i};if(i=i||{},B.isArray(c))for(o=0,s=c.length;o<s;++o)f[l=c[o]]=tt([i[l],d[l],h[l]],g,e,m);else for(o=0,s=(r=Object.keys(c)).length;o<s;++o)f[l=r[o]]=tt([i[l],d[c[l]],d[l],h[l]],g,e,m);return m.cacheable&&(n._cachedDataOpts=Object.freeze(f)),f},removeHoverStyle:function(t){B.merge(t._model,t.$previousStyle||{}),delete t.$previousStyle},setHoverStyle:function(t){var e=this.chart.data.datasets[t._datasetIndex],n=t._index,i=t.custom||{},a=t._model,r=B.getHoverColor;t.$previousStyle={backgroundColor:a.backgroundColor,borderColor:a.borderColor,borderWidth:a.borderWidth},a.backgroundColor=tt([i.hoverBackgroundColor,e.hoverBackgroundColor,r(a.backgroundColor)],void 0,n),a.borderColor=tt([i.hoverBorderColor,e.hoverBorderColor,r(a.borderColor)],void 0,n),a.borderWidth=tt([i.hoverBorderWidth,e.hoverBorderWidth,a.borderWidth],void 0,n)},_removeDatasetHoverStyle:function(){var t=this.getMeta().dataset;t&&this.removeHoverStyle(t)},_setDatasetHoverStyle:function(){var t,e,n,i,a,r,o=this.getMeta().dataset,s={};if(o){for(r=o._model,a=this._resolveDatasetElementOptions(o,!0),t=0,e=(i=Object.keys(a)).length;t<e;++t)s[n=i[t]]=r[n],r[n]=a[n];o.$previousStyle=s}},resyncElements:function(){var t=this.getMeta(),e=this.getDataset().data,n=t.data.length,i=e.length;i<n?t.data.splice(i,n-i):i>n&&this.insertElements(n,i-n)},insertElements:function(t,e){for(var n=0;n<e;++n)this.addElementAndReset(t+n)},onDataPush:function(){var t=arguments.length;this.insertElements(this.getDataset().data.length-t,t)},onDataPop:function(){this.getMeta().data.pop()},onDataShift:function(){this.getMeta().data.shift()},onDataSplice:function(t,e){this.getMeta().data.splice(t,e),this.insertElements(t,arguments.length-2)},onDataUnshift:function(){this.insertElements(0,arguments.length)}}),it.extend=B.inherits;var at=it,rt=2*Math.PI;function ot(t,e){var n=e.startAngle,i=e.endAngle,a=e.pixelMargin,r=a/e.outerRadius,o=e.x,s=e.y;t.beginPath(),t.arc(o,s,e.outerRadius,n-r,i+r),e.innerRadius>a?(r=a/e.innerRadius,t.arc(o,s,e.innerRadius-a,i+r,n-r,!0)):t.arc(o,s,a,i+Math.PI/2,n-Math.PI/2),t.closePath(),t.clip()}function st(t,e,n){var i="inner"===e.borderAlign;i?(t.lineWidth=2*e.borderWidth,t.lineJoin="round"):(t.lineWidth=e.borderWidth,t.lineJoin="bevel"),n.fullCircles&&function(t,e,n,i){var a,r=n.endAngle;for(i&&(n.endAngle=n.startAngle+rt,ot(t,n),n.endAngle=r,n.endAngle===n.startAngle&&n.fullCircles&&(n.endAngle+=rt,n.fullCircles--)),t.beginPath(),t.arc(n.x,n.y,n.innerRadius,n.startAngle+rt,n.startAngle,!0),a=0;a<n.fullCircles;++a)t.stroke();for(t.beginPath(),t.arc(n.x,n.y,e.outerRadius,n.startAngle,n.startAngle+rt),a=0;a<n.fullCircles;++a)t.stroke()}(t,e,n,i),i&&ot(t,n),t.beginPath(),t.arc(n.x,n.y,e.outerRadius,n.startAngle,n.endAngle),t.arc(n.x,n.y,n.innerRadius,n.endAngle,n.startAngle,!0),t.closePath(),t.stroke()}Y._set("global",{elements:{arc:{backgroundColor:Y.global.defaultColor,borderColor:"#fff",borderWidth:2,borderAlign:"center"}}});var lt=X.extend({_type:"arc",inLabelRange:function(t){var e=this._view;return!!e&&Math.pow(t-e.x,2)<Math.pow(e.radius+e.hoverRadius,2)},inRange:function(t,e){var n=this._view;if(n){for(var i=B.getAngleFromPoint(n,{x:t,y:e}),a=i.angle,r=i.distance,o=n.startAngle,s=n.endAngle;s<o;)s+=rt;for(;a>s;)a-=rt;for(;a<o;)a+=rt;var l=a>=o&&a<=s,u=r>=n.innerRadius&&r<=n.outerRadius;return l&&u}return!1},getCenterPoint:function(){var t=this._view,e=(t.startAngle+t.endAngle)/2,n=(t.innerRadius+t.outerRadius)/2;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},getArea:function(){var t=this._view;return Math.PI*((t.endAngle-t.startAngle)/(2*Math.PI))*(Math.pow(t.outerRadius,2)-Math.pow(t.innerRadius,2))},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,n=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},draw:function(){var t,e=this._chart.ctx,n=this._view,i="inner"===n.borderAlign?.33:0,a={x:n.x,y:n.y,innerRadius:n.innerRadius,outerRadius:Math.max(n.outerRadius-i,0),pixelMargin:i,startAngle:n.startAngle,endAngle:n.endAngle,fullCircles:Math.floor(n.circumference/rt)};if(e.save(),e.fillStyle=n.backgroundColor,e.strokeStyle=n.borderColor,a.fullCircles){for(a.endAngle=a.startAngle+rt,e.beginPath(),e.arc(a.x,a.y,a.outerRadius,a.startAngle,a.endAngle),e.arc(a.x,a.y,a.innerRadius,a.endAngle,a.startAngle,!0),e.closePath(),t=0;t<a.fullCircles;++t)e.fill();a.endAngle=a.startAngle+n.circumference%rt}e.beginPath(),e.arc(a.x,a.y,a.outerRadius,a.startAngle,a.endAngle),e.arc(a.x,a.y,a.innerRadius,a.endAngle,a.startAngle,!0),e.closePath(),e.fill(),n.borderWidth&&st(e,n,a),e.restore()}}),ut=B.valueOrDefault,dt=Y.global.defaultColor;Y._set("global",{elements:{line:{tension:.4,backgroundColor:dt,borderWidth:3,borderColor:dt,borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",capBezierPoints:!0,fill:!0}}});var ht=X.extend({_type:"line",draw:function(){var t,e,n,i=this,a=i._view,r=i._chart.ctx,o=a.spanGaps,s=i._children.slice(),l=Y.global,u=l.elements.line,d=-1,h=i._loop;if(s.length){if(i._loop){for(t=0;t<s.length;++t)if(e=B.previousItem(s,t),!s[t]._view.skip&&e._view.skip){s=s.slice(t).concat(s.slice(0,t)),h=o;break}h&&s.push(s[0])}for(r.save(),r.lineCap=a.borderCapStyle||u.borderCapStyle,r.setLineDash&&r.setLineDash(a.borderDash||u.borderDash),r.lineDashOffset=ut(a.borderDashOffset,u.borderDashOffset),r.lineJoin=a.borderJoinStyle||u.borderJoinStyle,r.lineWidth=ut(a.borderWidth,u.borderWidth),r.strokeStyle=a.borderColor||l.defaultColor,r.beginPath(),(n=s[0]._view).skip||(r.moveTo(n.x,n.y),d=0),t=1;t<s.length;++t)n=s[t]._view,e=-1===d?B.previousItem(s,t):s[d],n.skip||(d!==t-1&&!o||-1===d?r.moveTo(n.x,n.y):B.canvas.lineTo(r,e._view,n),d=t);h&&r.closePath(),r.stroke(),r.restore()}}}),ct=B.valueOrDefault,ft=Y.global.defaultColor;function gt(t){var e=this._view;return!!e&&Math.abs(t-e.x)<e.radius+e.hitRadius}Y._set("global",{elements:{point:{radius:3,pointStyle:"circle",backgroundColor:ft,borderColor:ft,borderWidth:1,hitRadius:1,hoverRadius:4,hoverBorderWidth:1}}});var mt=X.extend({_type:"point",inRange:function(t,e){var n=this._view;return!!n&&Math.pow(t-n.x,2)+Math.pow(e-n.y,2)<Math.pow(n.hitRadius+n.radius,2)},inLabelRange:gt,inXRange:gt,inYRange:function(t){var e=this._view;return!!e&&Math.abs(t-e.y)<e.radius+e.hitRadius},getCenterPoint:function(){var t=this._view;return{x:t.x,y:t.y}},getArea:function(){return Math.PI*Math.pow(this._view.radius,2)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y,padding:t.radius+t.borderWidth}},draw:function(t){var e=this._view,n=this._chart.ctx,i=e.pointStyle,a=e.rotation,r=e.radius,o=e.x,s=e.y,l=Y.global,u=l.defaultColor;e.skip||(void 0===t||B.canvas._isPointInArea(e,t))&&(n.strokeStyle=e.borderColor||u,n.lineWidth=ct(e.borderWidth,l.elements.point.borderWidth),n.fillStyle=e.backgroundColor||u,B.canvas.drawPoint(n,i,r,o,s,a))}}),pt=Y.global.defaultColor;function vt(t){return t&&void 0!==t.width}function bt(t){var e,n,i,a,r;return vt(t)?(r=t.width/2,e=t.x-r,n=t.x+r,i=Math.min(t.y,t.base),a=Math.max(t.y,t.base)):(r=t.height/2,e=Math.min(t.x,t.base),n=Math.max(t.x,t.base),i=t.y-r,a=t.y+r),{left:e,top:i,right:n,bottom:a}}function yt(t,e,n){return t===e?n:t===n?e:t}function xt(t,e,n){var i,a,r,o,s=t.borderWidth,l=function(t){var e=t.borderSkipped,n={};return e?(t.horizontal?t.base>t.x&&(e=yt(e,"left","right")):t.base<t.y&&(e=yt(e,"bottom","top")),n[e]=!0,n):n}(t);return B.isObject(s)?(i=+s.top||0,a=+s.right||0,r=+s.bottom||0,o=+s.left||0):i=a=r=o=+s||0,{t:l.top||i<0?0:i>n?n:i,r:l.right||a<0?0:a>e?e:a,b:l.bottom||r<0?0:r>n?n:r,l:l.left||o<0?0:o>e?e:o}}function _t(t,e,n){var i=null===e,a=null===n,r=!(!t||i&&a)&&bt(t);return r&&(i||e>=r.left&&e<=r.right)&&(a||n>=r.top&&n<=r.bottom)}Y._set("global",{elements:{rectangle:{backgroundColor:pt,borderColor:pt,borderSkipped:"bottom",borderWidth:0}}});var wt=X.extend({_type:"rectangle",draw:function(){var t=this._chart.ctx,e=this._view,n=function(t){var e=bt(t),n=e.right-e.left,i=e.bottom-e.top,a=xt(t,n/2,i/2);return{outer:{x:e.left,y:e.top,w:n,h:i},inner:{x:e.left+a.l,y:e.top+a.t,w:n-a.l-a.r,h:i-a.t-a.b}}}(e),i=n.outer,a=n.inner;t.fillStyle=e.backgroundColor,t.fillRect(i.x,i.y,i.w,i.h),i.w===a.w&&i.h===a.h||(t.save(),t.beginPath(),t.rect(i.x,i.y,i.w,i.h),t.clip(),t.fillStyle=e.borderColor,t.rect(a.x,a.y,a.w,a.h),t.fill("evenodd"),t.restore())},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){return _t(this._view,t,e)},inLabelRange:function(t,e){var n=this._view;return vt(n)?_t(n,t,null):_t(n,null,e)},inXRange:function(t){return _t(this._view,t,null)},inYRange:function(t){return _t(this._view,null,t)},getCenterPoint:function(){var t,e,n=this._view;return vt(n)?(t=n.x,e=(n.y+n.base)/2):(t=(n.x+n.base)/2,e=n.y),{x:t,y:e}},getArea:function(){var t=this._view;return vt(t)?t.width*Math.abs(t.y-t.base):t.height*Math.abs(t.x-t.base)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}}),kt={},Mt=lt,St=ht,Dt=mt,Ct=wt;kt.Arc=Mt,kt.Line=St,kt.Point=Dt,kt.Rectangle=Ct;var Pt=B._deprecated,Tt=B.valueOrDefault;function Ot(t,e,n){var i,a,r=n.barThickness,o=e.stackCount,s=e.pixels[t],l=B.isNullOrUndef(r)?function(t,e){var n,i,a,r,o=t._length;for(a=1,r=e.length;a<r;++a)o=Math.min(o,Math.abs(e[a]-e[a-1]));for(a=0,r=t.getTicks().length;a<r;++a)i=t.getPixelForTick(a),o=a>0?Math.min(o,Math.abs(i-n)):o,n=i;return o}(e.scale,e.pixels):-1;return B.isNullOrUndef(r)?(i=l*n.categoryPercentage,a=n.barPercentage):(i=r*o,a=1),{chunk:i/o,ratio:a,start:s-i/2}}Y._set("bar",{hover:{mode:"label"},scales:{xAxes:[{type:"category",offset:!0,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}}),Y._set("global",{datasets:{bar:{categoryPercentage:.8,barPercentage:.9}}});var At=at.extend({dataElementType:kt.Rectangle,_dataElementOptions:["backgroundColor","borderColor","borderSkipped","borderWidth","barPercentage","barThickness","categoryPercentage","maxBarThickness","minBarLength"],initialize:function(){var t,e,n=this;at.prototype.initialize.apply(n,arguments),(t=n.getMeta()).stack=n.getDataset().stack,t.bar=!0,e=n._getIndexScale().options,Pt("bar chart",e.barPercentage,"scales.[x/y]Axes.barPercentage","dataset.barPercentage"),Pt("bar chart",e.barThickness,"scales.[x/y]Axes.barThickness","dataset.barThickness"),Pt("bar chart",e.categoryPercentage,"scales.[x/y]Axes.categoryPercentage","dataset.categoryPercentage"),Pt("bar chart",n._getValueScale().options.minBarLength,"scales.[x/y]Axes.minBarLength","dataset.minBarLength"),Pt("bar chart",e.maxBarThickness,"scales.[x/y]Axes.maxBarThickness","dataset.maxBarThickness")},update:function(t){var e,n,i=this.getMeta().data;for(this._ruler=this.getRuler(),e=0,n=i.length;e<n;++e)this.updateElement(i[e],e,t)},updateElement:function(t,e,n){var i=this,a=i.getMeta(),r=i.getDataset(),o=i._resolveDataElementOptions(t,e);t._xScale=i.getScaleForId(a.xAxisID),t._yScale=i.getScaleForId(a.yAxisID),t._datasetIndex=i.index,t._index=e,t._model={backgroundColor:o.backgroundColor,borderColor:o.borderColor,borderSkipped:o.borderSkipped,borderWidth:o.borderWidth,datasetLabel:r.label,label:i.chart.data.labels[e]},B.isArray(r.data[e])&&(t._model.borderSkipped=null),i._updateElementGeometry(t,e,n,o),t.pivot()},_updateElementGeometry:function(t,e,n,i){var a=this,r=t._model,o=a._getValueScale(),s=o.getBasePixel(),l=o.isHorizontal(),u=a._ruler||a.getRuler(),d=a.calculateBarValuePixels(a.index,e,i),h=a.calculateBarIndexPixels(a.index,e,u,i);r.horizontal=l,r.base=n?s:d.base,r.x=l?n?s:d.head:h.center,r.y=l?h.center:n?s:d.head,r.height=l?h.size:void 0,r.width=l?void 0:h.size},_getStacks:function(t){var e,n,i=this._getIndexScale(),a=i._getMatchingVisibleMetas(this._type),r=i.options.stacked,o=a.length,s=[];for(e=0;e<o&&(n=a[e],(!1===r||-1===s.indexOf(n.stack)||void 0===r&&void 0===n.stack)&&s.push(n.stack),n.index!==t);++e);return s},getStackCount:function(){return this._getStacks().length},getStackIndex:function(t,e){var n=this._getStacks(t),i=void 0!==e?n.indexOf(e):-1;return-1===i?n.length-1:i},getRuler:function(){var t,e,n=this._getIndexScale(),i=[];for(t=0,e=this.getMeta().data.length;t<e;++t)i.push(n.getPixelForValue(null,t,this.index));return{pixels:i,start:n._startPixel,end:n._endPixel,stackCount:this.getStackCount(),scale:n}},calculateBarValuePixels:function(t,e,n){var i,a,r,o,s,l,u,d=this.chart,h=this._getValueScale(),c=h.isHorizontal(),f=d.data.datasets,g=h._getMatchingVisibleMetas(this._type),m=h._parseValue(f[t].data[e]),p=n.minBarLength,v=h.options.stacked,b=this.getMeta().stack,y=void 0===m.start?0:m.max>=0&&m.min>=0?m.min:m.max,x=void 0===m.start?m.end:m.max>=0&&m.min>=0?m.max-m.min:m.min-m.max,_=g.length;if(v||void 0===v&&void 0!==b)for(i=0;i<_&&(a=g[i]).index!==t;++i)a.stack===b&&(r=void 0===(u=h._parseValue(f[a.index].data[e])).start?u.end:u.min>=0&&u.max>=0?u.max:u.min,(m.min<0&&r<0||m.max>=0&&r>0)&&(y+=r));return o=h.getPixelForValue(y),l=(s=h.getPixelForValue(y+x))-o,void 0!==p&&Math.abs(l)<p&&(l=p,s=x>=0&&!c||x<0&&c?o-p:o+p),{size:l,base:o,head:s,center:s+l/2}},calculateBarIndexPixels:function(t,e,n,i){var a="flex"===i.barThickness?function(t,e,n){var i,a=e.pixels,r=a[t],o=t>0?a[t-1]:null,s=t<a.length-1?a[t+1]:null,l=n.categoryPercentage;return null===o&&(o=r-(null===s?e.end-e.start:s-r)),null===s&&(s=r+r-o),i=r-(r-Math.min(o,s))/2*l,{chunk:Math.abs(s-o)/2*l/e.stackCount,ratio:n.barPercentage,start:i}}(e,n,i):Ot(e,n,i),r=this.getStackIndex(t,this.getMeta().stack),o=a.start+a.chunk*r+a.chunk/2,s=Math.min(Tt(i.maxBarThickness,1/0),a.chunk*a.ratio);return{base:o-s/2,head:o+s/2,center:o,size:s}},draw:function(){var t=this.chart,e=this._getValueScale(),n=this.getMeta().data,i=this.getDataset(),a=n.length,r=0;for(B.canvas.clipArea(t.ctx,t.chartArea);r<a;++r){var o=e._parseValue(i.data[r]);isNaN(o.min)||isNaN(o.max)||n[r].draw()}B.canvas.unclipArea(t.ctx)},_resolveDataElementOptions:function(){var t=this,e=B.extend({},at.prototype._resolveDataElementOptions.apply(t,arguments)),n=t._getIndexScale().options,i=t._getValueScale().options;return e.barPercentage=Tt(n.barPercentage,e.barPercentage),e.barThickness=Tt(n.barThickness,e.barThickness),e.categoryPercentage=Tt(n.categoryPercentage,e.categoryPercentage),e.maxBarThickness=Tt(n.maxBarThickness,e.maxBarThickness),e.minBarLength=Tt(i.minBarLength,e.minBarLength),e}}),Ft=B.valueOrDefault,It=B.options.resolve;Y._set("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+": ("+t.xLabel+", "+t.yLabel+", "+i.r+")"}}}});var Lt=at.extend({dataElementType:kt.Point,_dataElementOptions:["backgroundColor","borderColor","borderWidth","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth","hoverRadius","hitRadius","pointStyle","rotation"],update:function(t){var e=this,n=e.getMeta().data;B.each(n,(function(n,i){e.updateElement(n,i,t)}))},updateElement:function(t,e,n){var i=this,a=i.getMeta(),r=t.custom||{},o=i.getScaleForId(a.xAxisID),s=i.getScaleForId(a.yAxisID),l=i._resolveDataElementOptions(t,e),u=i.getDataset().data[e],d=i.index,h=n?o.getPixelForDecimal(.5):o.getPixelForValue("object"==typeof u?u:NaN,e,d),c=n?s.getBasePixel():s.getPixelForValue(u,e,d);t._xScale=o,t._yScale=s,t._options=l,t._datasetIndex=d,t._index=e,t._model={backgroundColor:l.backgroundColor,borderColor:l.borderColor,borderWidth:l.borderWidth,hitRadius:l.hitRadius,pointStyle:l.pointStyle,rotation:l.rotation,radius:n?0:l.radius,skip:r.skip||isNaN(h)||isNaN(c),x:h,y:c},t.pivot()},setHoverStyle:function(t){var e=t._model,n=t._options,i=B.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth,radius:e.radius},e.backgroundColor=Ft(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=Ft(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=Ft(n.hoverBorderWidth,n.borderWidth),e.radius=n.radius+n.hoverRadius},_resolveDataElementOptions:function(t,e){var n=this,i=n.chart,a=n.getDataset(),r=t.custom||{},o=a.data[e]||{},s=at.prototype._resolveDataElementOptions.apply(n,arguments),l={chart:i,dataIndex:e,dataset:a,datasetIndex:n.index};return n._cachedDataOpts===s&&(s=B.extend({},s)),s.radius=It([r.radius,o.r,n._config.radius,i.options.elements.point.radius],l,e),s}}),Rt=B.valueOrDefault,Nt=Math.PI,Wt=2*Nt,Yt=Nt/2;Y._set("doughnut",{animation:{animateRotate:!0,animateScale:!1},hover:{mode:"single"},legendCallback:function(t){var e,n,i,a=document.createElement("ul"),r=t.data,o=r.datasets,s=r.labels;if(a.setAttribute("class",t.id+"-legend"),o.length)for(e=0,n=o[0].data.length;e<n;++e)(i=a.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=o[0].backgroundColor[e],s[e]&&i.appendChild(document.createTextNode(s[e]));return a.outerHTML},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map((function(n,i){var a=t.getDatasetMeta(0),r=a.controller.getStyle(i);return{text:n,fillStyle:r.backgroundColor,strokeStyle:r.borderColor,lineWidth:r.borderWidth,hidden:isNaN(e.datasets[0].data[i])||a.data[i].hidden,index:i}})):[]}},onClick:function(t,e){var n,i,a,r=e.index,o=this.chart;for(n=0,i=(o.data.datasets||[]).length;n<i;++n)(a=o.getDatasetMeta(n)).data[r]&&(a.data[r].hidden=!a.data[r].hidden);o.update()}},cutoutPercentage:50,rotation:-Yt,circumference:Wt,tooltips:{callbacks:{title:function(){return""},label:function(t,e){var n=e.labels[t.index],i=": "+e.datasets[t.datasetIndex].data[t.index];return B.isArray(n)?(n=n.slice())[0]+=i:n+=i,n}}}});var zt=at.extend({dataElementType:kt.Arc,linkScales:B.noop,_dataElementOptions:["backgroundColor","borderColor","borderWidth","borderAlign","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth"],getRingIndex:function(t){for(var e=0,n=0;n<t;++n)this.chart.isDatasetVisible(n)&&++e;return e},update:function(t){var e,n,i,a,r=this,o=r.chart,s=o.chartArea,l=o.options,u=1,d=1,h=0,c=0,f=r.getMeta(),g=f.data,m=l.cutoutPercentage/100||0,p=l.circumference,v=r._getRingWeight(r.index);if(p<Wt){var b=l.rotation%Wt,y=(b+=b>=Nt?-Wt:b<-Nt?Wt:0)+p,x=Math.cos(b),_=Math.sin(b),w=Math.cos(y),k=Math.sin(y),M=b<=0&&y>=0||y>=Wt,S=b<=Yt&&y>=Yt||y>=Wt+Yt,D=b<=-Yt&&y>=-Yt||y>=Nt+Yt,C=b===-Nt||y>=Nt?-1:Math.min(x,x*m,w,w*m),P=D?-1:Math.min(_,_*m,k,k*m),T=M?1:Math.max(x,x*m,w,w*m),O=S?1:Math.max(_,_*m,k,k*m);u=(T-C)/2,d=(O-P)/2,h=-(T+C)/2,c=-(O+P)/2}for(i=0,a=g.length;i<a;++i)g[i]._options=r._resolveDataElementOptions(g[i],i);for(o.borderWidth=r.getMaxBorderWidth(),e=(s.right-s.left-o.borderWidth)/u,n=(s.bottom-s.top-o.borderWidth)/d,o.outerRadius=Math.max(Math.min(e,n)/2,0),o.innerRadius=Math.max(o.outerRadius*m,0),o.radiusLength=(o.outerRadius-o.innerRadius)/(r._getVisibleDatasetWeightTotal()||1),o.offsetX=h*o.outerRadius,o.offsetY=c*o.outerRadius,f.total=r.calculateTotal(),r.outerRadius=o.outerRadius-o.radiusLength*r._getRingWeightOffset(r.index),r.innerRadius=Math.max(r.outerRadius-o.radiusLength*v,0),i=0,a=g.length;i<a;++i)r.updateElement(g[i],i,t)},updateElement:function(t,e,n){var i=this,a=i.chart,r=a.chartArea,o=a.options,s=o.animation,l=(r.left+r.right)/2,u=(r.top+r.bottom)/2,d=o.rotation,h=o.rotation,c=i.getDataset(),f=n&&s.animateRotate?0:t.hidden?0:i.calculateCircumference(c.data[e])*(o.circumference/Wt),g=n&&s.animateScale?0:i.innerRadius,m=n&&s.animateScale?0:i.outerRadius,p=t._options||{};B.extend(t,{_datasetIndex:i.index,_index:e,_model:{backgroundColor:p.backgroundColor,borderColor:p.borderColor,borderWidth:p.borderWidth,borderAlign:p.borderAlign,x:l+a.offsetX,y:u+a.offsetY,startAngle:d,endAngle:h,circumference:f,outerRadius:m,innerRadius:g,label:B.valueAtIndexOrDefault(c.label,e,a.data.labels[e])}});var v=t._model;n&&s.animateRotate||(v.startAngle=0===e?o.rotation:i.getMeta().data[e-1]._model.endAngle,v.endAngle=v.startAngle+v.circumference),t.pivot()},calculateTotal:function(){var t,e=this.getDataset(),n=this.getMeta(),i=0;return B.each(n.data,(function(n,a){t=e.data[a],isNaN(t)||n.hidden||(i+=Math.abs(t))})),i},calculateCircumference:function(t){var e=this.getMeta().total;return e>0&&!isNaN(t)?Wt*(Math.abs(t)/e):0},getMaxBorderWidth:function(t){var e,n,i,a,r,o,s,l,u=0,d=this.chart;if(!t)for(e=0,n=d.data.datasets.length;e<n;++e)if(d.isDatasetVisible(e)){t=(i=d.getDatasetMeta(e)).data,e!==this.index&&(r=i.controller);break}if(!t)return 0;for(e=0,n=t.length;e<n;++e)a=t[e],r?(r._configure(),o=r._resolveDataElementOptions(a,e)):o=a._options,"inner"!==o.borderAlign&&(s=o.borderWidth,u=(l=o.hoverBorderWidth)>(u=s>u?s:u)?l:u);return u},setHoverStyle:function(t){var e=t._model,n=t._options,i=B.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth},e.backgroundColor=Rt(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=Rt(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=Rt(n.hoverBorderWidth,n.borderWidth)},_getRingWeightOffset:function(t){for(var e=0,n=0;n<t;++n)this.chart.isDatasetVisible(n)&&(e+=this._getRingWeight(n));return e},_getRingWeight:function(t){return Math.max(Rt(this.chart.data.datasets[t].weight,1),0)},_getVisibleDatasetWeightTotal:function(){return this._getRingWeightOffset(this.chart.data.datasets.length)}});Y._set("horizontalBar",{hover:{mode:"index",axis:"y"},scales:{xAxes:[{type:"linear",position:"bottom"}],yAxes:[{type:"category",position:"left",offset:!0,gridLines:{offsetGridLines:!0}}]},elements:{rectangle:{borderSkipped:"left"}},tooltips:{mode:"index",axis:"y"}}),Y._set("global",{datasets:{horizontalBar:{categoryPercentage:.8,barPercentage:.9}}});var Et=At.extend({_getValueScaleId:function(){return this.getMeta().xAxisID},_getIndexScaleId:function(){return this.getMeta().yAxisID}}),Vt=B.valueOrDefault,Ht=B.options.resolve,Bt=B.canvas._isPointInArea;function jt(t,e){var n=t&&t.options.ticks||{},i=n.reverse,a=void 0===n.min?e:0,r=void 0===n.max?e:0;return{start:i?r:a,end:i?a:r}}function Ut(t,e,n){var i=n/2,a=jt(t,i),r=jt(e,i);return{top:r.end,right:a.end,bottom:r.start,left:a.start}}function Gt(t){var e,n,i,a;return B.isObject(t)?(e=t.top,n=t.right,i=t.bottom,a=t.left):e=n=i=a=t,{top:e,right:n,bottom:i,left:a}}Y._set("line",{showLines:!0,spanGaps:!1,hover:{mode:"label"},scales:{xAxes:[{type:"category",id:"x-axis-0"}],yAxes:[{type:"linear",id:"y-axis-0"}]}});var qt=at.extend({datasetElementType:kt.Line,dataElementType:kt.Point,_datasetElementOptions:["backgroundColor","borderCapStyle","borderColor","borderDash","borderDashOffset","borderJoinStyle","borderWidth","cubicInterpolationMode","fill"],_dataElementOptions:{backgroundColor:"pointBackgroundColor",borderColor:"pointBorderColor",borderWidth:"pointBorderWidth",hitRadius:"pointHitRadius",hoverBackgroundColor:"pointHoverBackgroundColor",hoverBorderColor:"pointHoverBorderColor",hoverBorderWidth:"pointHoverBorderWidth",hoverRadius:"pointHoverRadius",pointStyle:"pointStyle",radius:"pointRadius",rotation:"pointRotation"},update:function(t){var e,n,i=this,a=i.getMeta(),r=a.dataset,o=a.data||[],s=i.chart.options,l=i._config,u=i._showLine=Vt(l.showLine,s.showLines);for(i._xScale=i.getScaleForId(a.xAxisID),i._yScale=i.getScaleForId(a.yAxisID),u&&(void 0!==l.tension&&void 0===l.lineTension&&(l.lineTension=l.tension),r._scale=i._yScale,r._datasetIndex=i.index,r._children=o,r._model=i._resolveDatasetElementOptions(r),r.pivot()),e=0,n=o.length;e<n;++e)i.updateElement(o[e],e,t);for(u&&0!==r._model.tension&&i.updateBezierControlPoints(),e=0,n=o.length;e<n;++e)o[e].pivot()},updateElement:function(t,e,n){var i,a,r=this,o=r.getMeta(),s=t.custom||{},l=r.getDataset(),u=r.index,d=l.data[e],h=r._xScale,c=r._yScale,f=o.dataset._model,g=r._resolveDataElementOptions(t,e);i=h.getPixelForValue("object"==typeof d?d:NaN,e,u),a=n?c.getBasePixel():r.calculatePointY(d,e,u),t._xScale=h,t._yScale=c,t._options=g,t._datasetIndex=u,t._index=e,t._model={x:i,y:a,skip:s.skip||isNaN(i)||isNaN(a),radius:g.radius,pointStyle:g.pointStyle,rotation:g.rotation,backgroundColor:g.backgroundColor,borderColor:g.borderColor,borderWidth:g.borderWidth,tension:Vt(s.tension,f?f.tension:0),steppedLine:!!f&&f.steppedLine,hitRadius:g.hitRadius}},_resolveDatasetElementOptions:function(t){var e=this,n=e._config,i=t.custom||{},a=e.chart.options,r=a.elements.line,o=at.prototype._resolveDatasetElementOptions.apply(e,arguments);return o.spanGaps=Vt(n.spanGaps,a.spanGaps),o.tension=Vt(n.lineTension,r.tension),o.steppedLine=Ht([i.steppedLine,n.steppedLine,r.stepped]),o.clip=Gt(Vt(n.clip,Ut(e._xScale,e._yScale,o.borderWidth))),o},calculatePointY:function(t,e,n){var i,a,r,o,s,l,u,d=this.chart,h=this._yScale,c=0,f=0;if(h.options.stacked){for(s=+h.getRightValue(t),u=(l=d._getSortedVisibleDatasetMetas()).length,i=0;i<u&&(r=l[i]).index!==n;++i)a=d.data.datasets[r.index],"line"===r.type&&r.yAxisID===h.id&&((o=+h.getRightValue(a.data[e]))<0?f+=o||0:c+=o||0);return s<0?h.getPixelForValue(f+s):h.getPixelForValue(c+s)}return h.getPixelForValue(t)},updateBezierControlPoints:function(){var t,e,n,i,a=this.chart,r=this.getMeta(),o=r.dataset._model,s=a.chartArea,l=r.data||[];function u(t,e,n){return Math.max(Math.min(t,n),e)}if(o.spanGaps&&(l=l.filter((function(t){return!t._model.skip}))),"monotone"===o.cubicInterpolationMode)B.splineCurveMonotone(l);else for(t=0,e=l.length;t<e;++t)n=l[t]._model,i=B.splineCurve(B.previousItem(l,t)._model,n,B.nextItem(l,t)._model,o.tension),n.controlPointPreviousX=i.previous.x,n.controlPointPreviousY=i.previous.y,n.controlPointNextX=i.next.x,n.controlPointNextY=i.next.y;if(a.options.elements.line.capBezierPoints)for(t=0,e=l.length;t<e;++t)n=l[t]._model,Bt(n,s)&&(t>0&&Bt(l[t-1]._model,s)&&(n.controlPointPreviousX=u(n.controlPointPreviousX,s.left,s.right),n.controlPointPreviousY=u(n.controlPointPreviousY,s.top,s.bottom)),t<l.length-1&&Bt(l[t+1]._model,s)&&(n.controlPointNextX=u(n.controlPointNextX,s.left,s.right),n.controlPointNextY=u(n.controlPointNextY,s.top,s.bottom)))},draw:function(){var t,e=this.chart,n=this.getMeta(),i=n.data||[],a=e.chartArea,r=e.canvas,o=0,s=i.length;for(this._showLine&&(t=n.dataset._model.clip,B.canvas.clipArea(e.ctx,{left:!1===t.left?0:a.left-t.left,right:!1===t.right?r.width:a.right+t.right,top:!1===t.top?0:a.top-t.top,bottom:!1===t.bottom?r.height:a.bottom+t.bottom}),n.dataset.draw(),B.canvas.unclipArea(e.ctx));o<s;++o)i[o].draw(a)},setHoverStyle:function(t){var e=t._model,n=t._options,i=B.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth,radius:e.radius},e.backgroundColor=Vt(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=Vt(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=Vt(n.hoverBorderWidth,n.borderWidth),e.radius=Vt(n.hoverRadius,n.radius)}}),Zt=B.options.resolve;Y._set("polarArea",{scale:{type:"radialLinear",angleLines:{display:!1},gridLines:{circular:!0},pointLabels:{display:!1},ticks:{beginAtZero:!0}},animation:{animateRotate:!0,animateScale:!0},startAngle:-.5*Math.PI,legendCallback:function(t){var e,n,i,a=document.createElement("ul"),r=t.data,o=r.datasets,s=r.labels;if(a.setAttribute("class",t.id+"-legend"),o.length)for(e=0,n=o[0].data.length;e<n;++e)(i=a.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=o[0].backgroundColor[e],s[e]&&i.appendChild(document.createTextNode(s[e]));return a.outerHTML},legend:{labels:{generateLabels:function(t){var e=t.data;return e.labels.length&&e.datasets.length?e.labels.map((function(n,i){var a=t.getDatasetMeta(0),r=a.controller.getStyle(i);return{text:n,fillStyle:r.backgroundColor,strokeStyle:r.borderColor,lineWidth:r.borderWidth,hidden:isNaN(e.datasets[0].data[i])||a.data[i].hidden,index:i}})):[]}},onClick:function(t,e){var n,i,a,r=e.index,o=this.chart;for(n=0,i=(o.data.datasets||[]).length;n<i;++n)(a=o.getDatasetMeta(n)).data[r].hidden=!a.data[r].hidden;o.update()}},tooltips:{callbacks:{title:function(){return""},label:function(t,e){return e.labels[t.index]+": "+t.yLabel}}}});var $t=at.extend({dataElementType:kt.Arc,linkScales:B.noop,_dataElementOptions:["backgroundColor","borderColor","borderWidth","borderAlign","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth"],_getIndexScaleId:function(){return this.chart.scale.id},_getValueScaleId:function(){return this.chart.scale.id},update:function(t){var e,n,i,a=this,r=a.getDataset(),o=a.getMeta(),s=a.chart.options.startAngle||0,l=a._starts=[],u=a._angles=[],d=o.data;for(a._updateRadius(),o.count=a.countVisibleElements(),e=0,n=r.data.length;e<n;e++)l[e]=s,i=a._computeAngle(e),u[e]=i,s+=i;for(e=0,n=d.length;e<n;++e)d[e]._options=a._resolveDataElementOptions(d[e],e),a.updateElement(d[e],e,t)},_updateRadius:function(){var t=this,e=t.chart,n=e.chartArea,i=e.options,a=Math.min(n.right-n.left,n.bottom-n.top);e.outerRadius=Math.max(a/2,0),e.innerRadius=Math.max(i.cutoutPercentage?e.outerRadius/100*i.cutoutPercentage:1,0),e.radiusLength=(e.outerRadius-e.innerRadius)/e.getVisibleDatasetCount(),t.outerRadius=e.outerRadius-e.radiusLength*t.index,t.innerRadius=t.outerRadius-e.radiusLength},updateElement:function(t,e,n){var i=this,a=i.chart,r=i.getDataset(),o=a.options,s=o.animation,l=a.scale,u=a.data.labels,d=l.xCenter,h=l.yCenter,c=o.startAngle,f=t.hidden?0:l.getDistanceFromCenterForValue(r.data[e]),g=i._starts[e],m=g+(t.hidden?0:i._angles[e]),p=s.animateScale?0:l.getDistanceFromCenterForValue(r.data[e]),v=t._options||{};B.extend(t,{_datasetIndex:i.index,_index:e,_scale:l,_model:{backgroundColor:v.backgroundColor,borderColor:v.borderColor,borderWidth:v.borderWidth,borderAlign:v.borderAlign,x:d,y:h,innerRadius:0,outerRadius:n?p:f,startAngle:n&&s.animateRotate?c:g,endAngle:n&&s.animateRotate?c:m,label:B.valueAtIndexOrDefault(u,e,u[e])}}),t.pivot()},countVisibleElements:function(){var t=this.getDataset(),e=this.getMeta(),n=0;return B.each(e.data,(function(e,i){isNaN(t.data[i])||e.hidden||n++})),n},setHoverStyle:function(t){var e=t._model,n=t._options,i=B.getHoverColor,a=B.valueOrDefault;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth},e.backgroundColor=a(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=a(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=a(n.hoverBorderWidth,n.borderWidth)},_computeAngle:function(t){var e=this,n=this.getMeta().count,i=e.getDataset(),a=e.getMeta();if(isNaN(i.data[t])||a.data[t].hidden)return 0;var r={chart:e.chart,dataIndex:t,dataset:i,datasetIndex:e.index};return Zt([e.chart.options.elements.arc.angle,2*Math.PI/n],r,t)}});Y._set("pie",B.clone(Y.doughnut)),Y._set("pie",{cutoutPercentage:0});var Xt=zt,Kt=B.valueOrDefault;Y._set("radar",{spanGaps:!1,scale:{type:"radialLinear"},elements:{line:{fill:"start",tension:0}}});var Jt=at.extend({datasetElementType:kt.Line,dataElementType:kt.Point,linkScales:B.noop,_datasetElementOptions:["backgroundColor","borderWidth","borderColor","borderCapStyle","borderDash","borderDashOffset","borderJoinStyle","fill"],_dataElementOptions:{backgroundColor:"pointBackgroundColor",borderColor:"pointBorderColor",borderWidth:"pointBorderWidth",hitRadius:"pointHitRadius",hoverBackgroundColor:"pointHoverBackgroundColor",hoverBorderColor:"pointHoverBorderColor",hoverBorderWidth:"pointHoverBorderWidth",hoverRadius:"pointHoverRadius",pointStyle:"pointStyle",radius:"pointRadius",rotation:"pointRotation"},_getIndexScaleId:function(){return this.chart.scale.id},_getValueScaleId:function(){return this.chart.scale.id},update:function(t){var e,n,i=this,a=i.getMeta(),r=a.dataset,o=a.data||[],s=i.chart.scale,l=i._config;for(void 0!==l.tension&&void 0===l.lineTension&&(l.lineTension=l.tension),r._scale=s,r._datasetIndex=i.index,r._children=o,r._loop=!0,r._model=i._resolveDatasetElementOptions(r),r.pivot(),e=0,n=o.length;e<n;++e)i.updateElement(o[e],e,t);for(i.updateBezierControlPoints(),e=0,n=o.length;e<n;++e)o[e].pivot()},updateElement:function(t,e,n){var i=this,a=t.custom||{},r=i.getDataset(),o=i.chart.scale,s=o.getPointPositionForValue(e,r.data[e]),l=i._resolveDataElementOptions(t,e),u=i.getMeta().dataset._model,d=n?o.xCenter:s.x,h=n?o.yCenter:s.y;t._scale=o,t._options=l,t._datasetIndex=i.index,t._index=e,t._model={x:d,y:h,skip:a.skip||isNaN(d)||isNaN(h),radius:l.radius,pointStyle:l.pointStyle,rotation:l.rotation,backgroundColor:l.backgroundColor,borderColor:l.borderColor,borderWidth:l.borderWidth,tension:Kt(a.tension,u?u.tension:0),hitRadius:l.hitRadius}},_resolveDatasetElementOptions:function(){var t=this,e=t._config,n=t.chart.options,i=at.prototype._resolveDatasetElementOptions.apply(t,arguments);return i.spanGaps=Kt(e.spanGaps,n.spanGaps),i.tension=Kt(e.lineTension,n.elements.line.tension),i},updateBezierControlPoints:function(){var t,e,n,i,a=this.getMeta(),r=this.chart.chartArea,o=a.data||[];function s(t,e,n){return Math.max(Math.min(t,n),e)}for(a.dataset._model.spanGaps&&(o=o.filter((function(t){return!t._model.skip}))),t=0,e=o.length;t<e;++t)n=o[t]._model,i=B.splineCurve(B.previousItem(o,t,!0)._model,n,B.nextItem(o,t,!0)._model,n.tension),n.controlPointPreviousX=s(i.previous.x,r.left,r.right),n.controlPointPreviousY=s(i.previous.y,r.top,r.bottom),n.controlPointNextX=s(i.next.x,r.left,r.right),n.controlPointNextY=s(i.next.y,r.top,r.bottom)},setHoverStyle:function(t){var e=t._model,n=t._options,i=B.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth,radius:e.radius},e.backgroundColor=Kt(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=Kt(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=Kt(n.hoverBorderWidth,n.borderWidth),e.radius=Kt(n.hoverRadius,n.radius)}});Y._set("scatter",{hover:{mode:"single"},scales:{xAxes:[{id:"x-axis-1",type:"linear",position:"bottom"}],yAxes:[{id:"y-axis-1",type:"linear",position:"left"}]},tooltips:{callbacks:{title:function(){return""},label:function(t){return"("+t.xLabel+", "+t.yLabel+")"}}}}),Y._set("global",{datasets:{scatter:{showLine:!1}}});var Qt={bar:At,bubble:Lt,doughnut:zt,horizontalBar:Et,line:qt,polarArea:$t,pie:Xt,radar:Jt,scatter:qt};function te(t,e){return t.native?{x:t.x,y:t.y}:B.getRelativePosition(t,e)}function ee(t,e){var n,i,a,r,o,s,l=t._getSortedVisibleDatasetMetas();for(i=0,r=l.length;i<r;++i)for(a=0,o=(n=l[i].data).length;a<o;++a)(s=n[a])._view.skip||e(s)}function ne(t,e){var n=[];return ee(t,(function(t){t.inRange(e.x,e.y)&&n.push(t)})),n}function ie(t,e,n,i){var a=Number.POSITIVE_INFINITY,r=[];return ee(t,(function(t){if(!n||t.inRange(e.x,e.y)){var o=t.getCenterPoint(),s=i(e,o);s<a?(r=[t],a=s):s===a&&r.push(t)}})),r}function ae(t){var e=-1!==t.indexOf("x"),n=-1!==t.indexOf("y");return function(t,i){var a=e?Math.abs(t.x-i.x):0,r=n?Math.abs(t.y-i.y):0;return Math.sqrt(Math.pow(a,2)+Math.pow(r,2))}}function re(t,e,n){var i=te(e,t);n.axis=n.axis||"x";var a=ae(n.axis),r=n.intersect?ne(t,i):ie(t,i,!1,a),o=[];return r.length?(t._getSortedVisibleDatasetMetas().forEach((function(t){var e=t.data[r[0]._index];e&&!e._view.skip&&o.push(e)})),o):[]}var oe={modes:{single:function(t,e){var n=te(e,t),i=[];return ee(t,(function(t){if(t.inRange(n.x,n.y))return i.push(t),i})),i.slice(0,1)},label:re,index:re,dataset:function(t,e,n){var i=te(e,t);n.axis=n.axis||"xy";var a=ae(n.axis),r=n.intersect?ne(t,i):ie(t,i,!1,a);return r.length>0&&(r=t.getDatasetMeta(r[0]._datasetIndex).data),r},"x-axis":function(t,e){return re(t,e,{intersect:!1})},point:function(t,e){return ne(t,te(e,t))},nearest:function(t,e,n){var i=te(e,t);n.axis=n.axis||"xy";var a=ae(n.axis);return ie(t,i,n.intersect,a)},x:function(t,e,n){var i=te(e,t),a=[],r=!1;return ee(t,(function(t){t.inXRange(i.x)&&a.push(t),t.inRange(i.x,i.y)&&(r=!0)})),n.intersect&&!r&&(a=[]),a},y:function(t,e,n){var i=te(e,t),a=[],r=!1;return ee(t,(function(t){t.inYRange(i.y)&&a.push(t),t.inRange(i.x,i.y)&&(r=!0)})),n.intersect&&!r&&(a=[]),a}}},se=B.extend;function le(t,e){return B.where(t,(function(t){return t.pos===e}))}function ue(t,e){return t.sort((function(t,n){var i=e?n:t,a=e?t:n;return i.weight===a.weight?i.index-a.index:i.weight-a.weight}))}function de(t,e,n,i){return Math.max(t[n],e[n])+Math.max(t[i],e[i])}function he(t,e,n){var i,a,r=n.box,o=t.maxPadding;if(n.size&&(t[n.pos]-=n.size),n.size=n.horizontal?r.height:r.width,t[n.pos]+=n.size,r.getPadding){var s=r.getPadding();o.top=Math.max(o.top,s.top),o.left=Math.max(o.left,s.left),o.bottom=Math.max(o.bottom,s.bottom),o.right=Math.max(o.right,s.right)}if(i=e.outerWidth-de(o,t,"left","right"),a=e.outerHeight-de(o,t,"top","bottom"),i!==t.w||a!==t.h){t.w=i,t.h=a;var l=n.horizontal?[i,t.w]:[a,t.h];return!(l[0]===l[1]||isNaN(l[0])&&isNaN(l[1]))}}function ce(t,e){var n=e.maxPadding;function i(t){var i={left:0,top:0,right:0,bottom:0};return t.forEach((function(t){i[t]=Math.max(e[t],n[t])})),i}return i(t?["left","right"]:["top","bottom"])}function fe(t,e,n){var i,a,r,o,s,l,u=[];for(i=0,a=t.length;i<a;++i)(o=(r=t[i]).box).update(r.width||e.w,r.height||e.h,ce(r.horizontal,e)),he(e,n,r)&&(l=!0,u.length&&(s=!0)),o.fullWidth||u.push(r);return s&&fe(u,e,n)||l}function ge(t,e,n){var i,a,r,o,s=n.padding,l=e.x,u=e.y;for(i=0,a=t.length;i<a;++i)o=(r=t[i]).box,r.horizontal?(o.left=o.fullWidth?s.left:e.left,o.right=o.fullWidth?n.outerWidth-s.right:e.left+e.w,o.top=u,o.bottom=u+o.height,o.width=o.right-o.left,u=o.bottom):(o.left=l,o.right=l+o.width,o.top=e.top,o.bottom=e.top+e.h,o.height=o.bottom-o.top,l=o.right);e.x=l,e.y=u}Y._set("global",{layout:{padding:{top:0,right:0,bottom:0,left:0}}});var me,pe={defaults:{},addBox:function(t,e){t.boxes||(t.boxes=[]),e.fullWidth=e.fullWidth||!1,e.position=e.position||"top",e.weight=e.weight||0,e._layers=e._layers||function(){return[{z:0,draw:function(){e.draw.apply(e,arguments)}}]},t.boxes.push(e)},removeBox:function(t,e){var n=t.boxes?t.boxes.indexOf(e):-1;-1!==n&&t.boxes.splice(n,1)},configure:function(t,e,n){for(var i,a=["fullWidth","position","weight"],r=a.length,o=0;o<r;++o)i=a[o],n.hasOwnProperty(i)&&(e[i]=n[i])},update:function(t,e,n){if(t){var i=t.options.layout||{},a=B.options.toPadding(i.padding),r=e-a.width,o=n-a.height,s=function(t){var e=function(t){var e,n,i,a=[];for(e=0,n=(t||[]).length;e<n;++e)i=t[e],a.push({index:e,box:i,pos:i.position,horizontal:i.isHorizontal(),weight:i.weight});return a}(t),n=ue(le(e,"left"),!0),i=ue(le(e,"right")),a=ue(le(e,"top"),!0),r=ue(le(e,"bottom"));return{leftAndTop:n.concat(a),rightAndBottom:i.concat(r),chartArea:le(e,"chartArea"),vertical:n.concat(i),horizontal:a.concat(r)}}(t.boxes),l=s.vertical,u=s.horizontal,d=Object.freeze({outerWidth:e,outerHeight:n,padding:a,availableWidth:r,vBoxMaxWidth:r/2/l.length,hBoxMaxHeight:o/2}),h=se({maxPadding:se({},a),w:r,h:o,x:a.left,y:a.top},a);!function(t,e){var n,i,a;for(n=0,i=t.length;n<i;++n)(a=t[n]).width=a.horizontal?a.box.fullWidth&&e.availableWidth:e.vBoxMaxWidth,a.height=a.horizontal&&e.hBoxMaxHeight}(l.concat(u),d),fe(l,h,d),fe(u,h,d)&&fe(l,h,d),function(t){var e=t.maxPadding;function n(n){var i=Math.max(e[n]-t[n],0);return t[n]+=i,i}t.y+=n("top"),t.x+=n("left"),n("right"),n("bottom")}(h),ge(s.leftAndTop,h,d),h.x+=h.w,h.y+=h.h,ge(s.rightAndBottom,h,d),t.chartArea={left:h.left,top:h.top,right:h.left+h.w,bottom:h.top+h.h},B.each(s.chartArea,(function(e){var n=e.box;se(n,t.chartArea),n.update(h.w,h.h)}))}}},ve=(me=Object.freeze({__proto__:null,default:"@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}"}))&&me.default||me,be="$chartjs",ye="chartjs-size-monitor",xe="chartjs-render-monitor",_e="chartjs-render-animation",we=["animationstart","webkitAnimationStart"],ke={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};function Me(t,e){var n=B.getStyle(t,e),i=n&&n.match(/^(\d+)(\.\d+)?px$/);return i?Number(i[1]):void 0}var Se=!!function(){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("e",null,e)}catch(t){}return t}()&&{passive:!0};function De(t,e,n){t.addEventListener(e,n,Se)}function Ce(t,e,n){t.removeEventListener(e,n,Se)}function Pe(t,e,n,i,a){return{type:t,chart:e,native:a||null,x:void 0!==n?n:null,y:void 0!==i?i:null}}function Te(t){var e=document.createElement("div");return e.className=t||"",e}function Oe(t,e,n){var i,a,r,o,s=t[be]||(t[be]={}),l=s.resizer=function(t){var e=Te(ye),n=Te(ye+"-expand"),i=Te(ye+"-shrink");n.appendChild(Te()),i.appendChild(Te()),e.appendChild(n),e.appendChild(i),e._reset=function(){n.scrollLeft=1e6,n.scrollTop=1e6,i.scrollLeft=1e6,i.scrollTop=1e6};var a=function(){e._reset(),t()};return De(n,"scroll",a.bind(n,"expand")),De(i,"scroll",a.bind(i,"shrink")),e}((i=function(){if(s.resizer){var i=n.options.maintainAspectRatio&&t.parentNode,a=i?i.clientWidth:0;e(Pe("resize",n)),i&&i.clientWidth<a&&n.canvas&&e(Pe("resize",n))}},r=!1,o=[],function(){o=Array.prototype.slice.call(arguments),a=a||this,r||(r=!0,B.requestAnimFrame.call(window,(function(){r=!1,i.apply(a,o)})))}));!function(t,e){var n=t[be]||(t[be]={}),i=n.renderProxy=function(t){t.animationName===_e&&e()};B.each(we,(function(e){De(t,e,i)})),n.reflow=!!t.offsetParent,t.classList.add(xe)}(t,(function(){if(s.resizer){var e=t.parentNode;e&&e!==l.parentNode&&e.insertBefore(l,e.firstChild),l._reset()}}))}function Ae(t){var e=t[be]||{},n=e.resizer;delete e.resizer,function(t){var e=t[be]||{},n=e.renderProxy;n&&(B.each(we,(function(e){Ce(t,e,n)})),delete e.renderProxy),t.classList.remove(xe)}(t),n&&n.parentNode&&n.parentNode.removeChild(n)}var Fe={disableCSSInjection:!1,_enabled:"undefined"!=typeof window&&"undefined"!=typeof document,_ensureLoaded:function(t){if(!this.disableCSSInjection){var e=t.getRootNode?t.getRootNode():document;!function(t,e){var n=t[be]||(t[be]={});if(!n.containsStyles){n.containsStyles=!0,e="/* Chart.js */\n"+e;var i=document.createElement("style");i.setAttribute("type","text/css"),i.appendChild(document.createTextNode(e)),t.appendChild(i)}}(e.host?e:document.head,ve)}},acquireContext:function(t,e){"string"==typeof t?t=document.getElementById(t):t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas);var n=t&&t.getContext&&t.getContext("2d");return n&&n.canvas===t?(this._ensureLoaded(t),function(t,e){var n=t.style,i=t.getAttribute("height"),a=t.getAttribute("width");if(t[be]={initial:{height:i,width:a,style:{display:n.display,height:n.height,width:n.width}}},n.display=n.display||"block",null===a||""===a){var r=Me(t,"width");void 0!==r&&(t.width=r)}if(null===i||""===i)if(""===t.style.height)t.height=t.width/(e.options.aspectRatio||2);else{var o=Me(t,"height");void 0!==r&&(t.height=o)}}(t,e),n):null},releaseContext:function(t){var e=t.canvas;if(e[be]){var n=e[be].initial;["height","width"].forEach((function(t){var i=n[t];B.isNullOrUndef(i)?e.removeAttribute(t):e.setAttribute(t,i)})),B.each(n.style||{},(function(t,n){e.style[n]=t})),e.width=e.width,delete e[be]}},addEventListener:function(t,e,n){var i=t.canvas;if("resize"!==e){var a=n[be]||(n[be]={});De(i,e,(a.proxies||(a.proxies={}))[t.id+"_"+e]=function(e){n(function(t,e){var n=ke[t.type]||t.type,i=B.getRelativePosition(t,e);return Pe(n,e,i.x,i.y,t)}(e,t))})}else Oe(i,n,t)},removeEventListener:function(t,e,n){var i=t.canvas;if("resize"!==e){var a=((n[be]||{}).proxies||{})[t.id+"_"+e];a&&Ce(i,e,a)}else Ae(i)}};B.addEvent=De,B.removeEvent=Ce;var Ie=Fe._enabled?Fe:{acquireContext:function(t){return t&&t.canvas&&(t=t.canvas),t&&t.getContext("2d")||null}},Le=B.extend({initialize:function(){},acquireContext:function(){},releaseContext:function(){},addEventListener:function(){},removeEventListener:function(){}},Ie);Y._set("global",{plugins:{}});var Re={_plugins:[],_cacheId:0,register:function(t){var e=this._plugins;[].concat(t).forEach((function(t){-1===e.indexOf(t)&&e.push(t)})),this._cacheId++},unregister:function(t){var e=this._plugins;[].concat(t).forEach((function(t){var n=e.indexOf(t);-1!==n&&e.splice(n,1)})),this._cacheId++},clear:function(){this._plugins=[],this._cacheId++},count:function(){return this._plugins.length},getAll:function(){return this._plugins},notify:function(t,e,n){var i,a,r,o,s,l=this.descriptors(t),u=l.length;for(i=0;i<u;++i)if("function"==typeof(s=(r=(a=l[i]).plugin)[e])&&((o=[t].concat(n||[])).push(a.options),!1===s.apply(r,o)))return!1;return!0},descriptors:function(t){var e=t.$plugins||(t.$plugins={});if(e.id===this._cacheId)return e.descriptors;var n=[],i=[],a=t&&t.config||{},r=a.options&&a.options.plugins||{};return this._plugins.concat(a.plugins||[]).forEach((function(t){if(-1===n.indexOf(t)){var e=t.id,a=r[e];!1!==a&&(!0===a&&(a=B.clone(Y.global.plugins[e])),n.push(t),i.push({plugin:t,options:a||{}}))}})),e.descriptors=i,e.id=this._cacheId,i},_invalidate:function(t){delete t.$plugins}},Ne={constructors:{},defaults:{},registerScaleType:function(t,e,n){this.constructors[t]=e,this.defaults[t]=B.clone(n)},getScaleConstructor:function(t){return this.constructors.hasOwnProperty(t)?this.constructors[t]:void 0},getScaleDefaults:function(t){return this.defaults.hasOwnProperty(t)?B.merge(Object.create(null),[Y.scale,this.defaults[t]]):{}},updateScaleDefaults:function(t,e){this.defaults.hasOwnProperty(t)&&(this.defaults[t]=B.extend(this.defaults[t],e))},addScalesToLayout:function(t){B.each(t.scales,(function(e){e.fullWidth=e.options.fullWidth,e.position=e.options.position,e.weight=e.options.weight,pe.addBox(t,e)}))}},We=B.valueOrDefault,Ye=B.rtl.getRtlAdapter;Y._set("global",{tooltips:{enabled:!0,custom:null,mode:"nearest",position:"average",intersect:!0,backgroundColor:"rgba(0,0,0,0.8)",titleFontStyle:"bold",titleSpacing:2,titleMarginBottom:6,titleFontColor:"#fff",titleAlign:"left",bodySpacing:2,bodyFontColor:"#fff",bodyAlign:"left",footerFontStyle:"bold",footerSpacing:2,footerMarginTop:6,footerFontColor:"#fff",footerAlign:"left",yPadding:6,xPadding:6,caretPadding:2,caretSize:5,cornerRadius:6,multiKeyBackground:"#fff",displayColors:!0,borderColor:"rgba(0,0,0,0)",borderWidth:0,callbacks:{beforeTitle:B.noop,title:function(t,e){var n="",i=e.labels,a=i?i.length:0;if(t.length>0){var r=t[0];r.label?n=r.label:r.xLabel?n=r.xLabel:a>0&&r.index<a&&(n=i[r.index])}return n},afterTitle:B.noop,beforeBody:B.noop,beforeLabel:B.noop,label:function(t,e){var n=e.datasets[t.datasetIndex].label||"";return n&&(n+=": "),B.isNullOrUndef(t.value)?n+=t.yLabel:n+=t.value,n},labelColor:function(t,e){var n=e.getDatasetMeta(t.datasetIndex).data[t.index]._view;return{borderColor:n.borderColor,backgroundColor:n.backgroundColor}},labelTextColor:function(){return this._options.bodyFontColor},afterLabel:B.noop,afterBody:B.noop,beforeFooter:B.noop,footer:B.noop,afterFooter:B.noop}}});var ze={average:function(t){if(!t.length)return!1;var e,n,i=0,a=0,r=0;for(e=0,n=t.length;e<n;++e){var o=t[e];if(o&&o.hasValue()){var s=o.tooltipPosition();i+=s.x,a+=s.y,++r}}return{x:i/r,y:a/r}},nearest:function(t,e){var n,i,a,r=e.x,o=e.y,s=Number.POSITIVE_INFINITY;for(n=0,i=t.length;n<i;++n){var l=t[n];if(l&&l.hasValue()){var u=l.getCenterPoint(),d=B.distanceBetweenPoints(e,u);d<s&&(s=d,a=l)}}if(a){var h=a.tooltipPosition();r=h.x,o=h.y}return{x:r,y:o}}};function Ee(t,e){return e&&(B.isArray(e)?Array.prototype.push.apply(t,e):t.push(e)),t}function Ve(t){return("string"==typeof t||t instanceof String)&&t.indexOf("\n")>-1?t.split("\n"):t}function He(t){var e=Y.global;return{xPadding:t.xPadding,yPadding:t.yPadding,xAlign:t.xAlign,yAlign:t.yAlign,rtl:t.rtl,textDirection:t.textDirection,bodyFontColor:t.bodyFontColor,_bodyFontFamily:We(t.bodyFontFamily,e.defaultFontFamily),_bodyFontStyle:We(t.bodyFontStyle,e.defaultFontStyle),_bodyAlign:t.bodyAlign,bodyFontSize:We(t.bodyFontSize,e.defaultFontSize),bodySpacing:t.bodySpacing,titleFontColor:t.titleFontColor,_titleFontFamily:We(t.titleFontFamily,e.defaultFontFamily),_titleFontStyle:We(t.titleFontStyle,e.defaultFontStyle),titleFontSize:We(t.titleFontSize,e.defaultFontSize),_titleAlign:t.titleAlign,titleSpacing:t.titleSpacing,titleMarginBottom:t.titleMarginBottom,footerFontColor:t.footerFontColor,_footerFontFamily:We(t.footerFontFamily,e.defaultFontFamily),_footerFontStyle:We(t.footerFontStyle,e.defaultFontStyle),footerFontSize:We(t.footerFontSize,e.defaultFontSize),_footerAlign:t.footerAlign,footerSpacing:t.footerSpacing,footerMarginTop:t.footerMarginTop,caretSize:t.caretSize,cornerRadius:t.cornerRadius,backgroundColor:t.backgroundColor,opacity:0,legendColorBackground:t.multiKeyBackground,displayColors:t.displayColors,borderColor:t.borderColor,borderWidth:t.borderWidth}}function Be(t,e){return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-t.xPadding:t.x+t.xPadding}function je(t){return Ee([],Ve(t))}var Ue=X.extend({initialize:function(){this._model=He(this._options),this._lastActive=[]},getTitle:function(){var t=this,e=t._options,n=e.callbacks,i=n.beforeTitle.apply(t,arguments),a=n.title.apply(t,arguments),r=n.afterTitle.apply(t,arguments),o=[];return o=Ee(o,Ve(i)),o=Ee(o,Ve(a)),o=Ee(o,Ve(r))},getBeforeBody:function(){return je(this._options.callbacks.beforeBody.apply(this,arguments))},getBody:function(t,e){var n=this,i=n._options.callbacks,a=[];return B.each(t,(function(t){var r={before:[],lines:[],after:[]};Ee(r.before,Ve(i.beforeLabel.call(n,t,e))),Ee(r.lines,i.label.call(n,t,e)),Ee(r.after,Ve(i.afterLabel.call(n,t,e))),a.push(r)})),a},getAfterBody:function(){return je(this._options.callbacks.afterBody.apply(this,arguments))},getFooter:function(){var t=this,e=t._options.callbacks,n=e.beforeFooter.apply(t,arguments),i=e.footer.apply(t,arguments),a=e.afterFooter.apply(t,arguments),r=[];return r=Ee(r,Ve(n)),r=Ee(r,Ve(i)),r=Ee(r,Ve(a))},update:function(t){var e,n,i,a,r,o,s,l,u,d,h=this,c=h._options,f=h._model,g=h._model=He(c),m=h._active,p=h._data,v={xAlign:f.xAlign,yAlign:f.yAlign},b={x:f.x,y:f.y},y={width:f.width,height:f.height},x={x:f.caretX,y:f.caretY};if(m.length){g.opacity=1;var _=[],w=[];x=ze[c.position].call(h,m,h._eventPosition);var k=[];for(e=0,n=m.length;e<n;++e)k.push((i=m[e],a=void 0,r=void 0,o=void 0,s=void 0,l=void 0,u=void 0,d=void 0,a=i._xScale,r=i._yScale||i._scale,o=i._index,s=i._datasetIndex,l=i._chart.getDatasetMeta(s).controller,u=l._getIndexScale(),d=l._getValueScale(),{xLabel:a?a.getLabelForIndex(o,s):"",yLabel:r?r.getLabelForIndex(o,s):"",label:u?""+u.getLabelForIndex(o,s):"",value:d?""+d.getLabelForIndex(o,s):"",index:o,datasetIndex:s,x:i._model.x,y:i._model.y}));c.filter&&(k=k.filter((function(t){return c.filter(t,p)}))),c.itemSort&&(k=k.sort((function(t,e){return c.itemSort(t,e,p)}))),B.each(k,(function(t){_.push(c.callbacks.labelColor.call(h,t,h._chart)),w.push(c.callbacks.labelTextColor.call(h,t,h._chart))})),g.title=h.getTitle(k,p),g.beforeBody=h.getBeforeBody(k,p),g.body=h.getBody(k,p),g.afterBody=h.getAfterBody(k,p),g.footer=h.getFooter(k,p),g.x=x.x,g.y=x.y,g.caretPadding=c.caretPadding,g.labelColors=_,g.labelTextColors=w,g.dataPoints=k,y=function(t,e){var n=t._chart.ctx,i=2*e.yPadding,a=0,r=e.body,o=r.reduce((function(t,e){return t+e.before.length+e.lines.length+e.after.length}),0);o+=e.beforeBody.length+e.afterBody.length;var s=e.title.length,l=e.footer.length,u=e.titleFontSize,d=e.bodyFontSize,h=e.footerFontSize;i+=s*u,i+=s?(s-1)*e.titleSpacing:0,i+=s?e.titleMarginBottom:0,i+=o*d,i+=o?(o-1)*e.bodySpacing:0,i+=l?e.footerMarginTop:0,i+=l*h,i+=l?(l-1)*e.footerSpacing:0;var c=0,f=function(t){a=Math.max(a,n.measureText(t).width+c)};return n.font=B.fontString(u,e._titleFontStyle,e._titleFontFamily),B.each(e.title,f),n.font=B.fontString(d,e._bodyFontStyle,e._bodyFontFamily),B.each(e.beforeBody.concat(e.afterBody),f),c=e.displayColors?d+2:0,B.each(r,(function(t){B.each(t.before,f),B.each(t.lines,f),B.each(t.after,f)})),c=0,n.font=B.fontString(h,e._footerFontStyle,e._footerFontFamily),B.each(e.footer,f),{width:a+=2*e.xPadding,height:i}}(this,g),b=function(t,e,n,i){var a=t.x,r=t.y,o=t.caretSize,s=t.caretPadding,l=t.cornerRadius,u=n.xAlign,d=n.yAlign,h=o+s,c=l+s;return"right"===u?a-=e.width:"center"===u&&((a-=e.width/2)+e.width>i.width&&(a=i.width-e.width),a<0&&(a=0)),"top"===d?r+=h:r-="bottom"===d?e.height+h:e.height/2,"center"===d?"left"===u?a+=h:"right"===u&&(a-=h):"left"===u?a-=c:"right"===u&&(a+=c),{x:a,y:r}}(g,y,v=function(t,e){var n,i,a,r,o,s=t._model,l=t._chart,u=t._chart.chartArea,d="center",h="center";s.y<e.height?h="top":s.y>l.height-e.height&&(h="bottom");var c=(u.left+u.right)/2,f=(u.top+u.bottom)/2;"center"===h?(n=function(t){return t<=c},i=function(t){return t>c}):(n=function(t){return t<=e.width/2},i=function(t){return t>=l.width-e.width/2}),a=function(t){return t+e.width+s.caretSize+s.caretPadding>l.width},r=function(t){return t-e.width-s.caretSize-s.caretPadding<0},o=function(t){return t<=f?"top":"bottom"},n(s.x)?(d="left",a(s.x)&&(d="center",h=o(s.y))):i(s.x)&&(d="right",r(s.x)&&(d="center",h=o(s.y)));var g=t._options;return{xAlign:g.xAlign?g.xAlign:d,yAlign:g.yAlign?g.yAlign:h}}(this,y),h._chart)}else g.opacity=0;return g.xAlign=v.xAlign,g.yAlign=v.yAlign,g.x=b.x,g.y=b.y,g.width=y.width,g.height=y.height,g.caretX=x.x,g.caretY=x.y,h._model=g,t&&c.custom&&c.custom.call(h,g),h},drawCaret:function(t,e){var n=this._chart.ctx,i=this._view,a=this.getCaretPosition(t,e,i);n.lineTo(a.x1,a.y1),n.lineTo(a.x2,a.y2),n.lineTo(a.x3,a.y3)},getCaretPosition:function(t,e,n){var i,a,r,o,s,l,u=n.caretSize,d=n.cornerRadius,h=n.xAlign,c=n.yAlign,f=t.x,g=t.y,m=e.width,p=e.height;if("center"===c)s=g+p/2,"left"===h?(a=(i=f)-u,r=i,o=s+u,l=s-u):(a=(i=f+m)+u,r=i,o=s-u,l=s+u);else if("left"===h?(i=(a=f+d+u)-u,r=a+u):"right"===h?(i=(a=f+m-d-u)-u,r=a+u):(i=(a=n.caretX)-u,r=a+u),"top"===c)s=(o=g)-u,l=o;else{s=(o=g+p)+u,l=o;var v=r;r=i,i=v}return{x1:i,x2:a,x3:r,y1:o,y2:s,y3:l}},drawTitle:function(t,e,n){var i,a,r,o=e.title,s=o.length;if(s){var l=Ye(e.rtl,e.x,e.width);for(t.x=Be(e,e._titleAlign),n.textAlign=l.textAlign(e._titleAlign),n.textBaseline="middle",i=e.titleFontSize,a=e.titleSpacing,n.fillStyle=e.titleFontColor,n.font=B.fontString(i,e._titleFontStyle,e._titleFontFamily),r=0;r<s;++r)n.fillText(o[r],l.x(t.x),t.y+i/2),t.y+=i+a,r+1===s&&(t.y+=e.titleMarginBottom-a)}},drawBody:function(t,e,n){var i,a,r,o,s,l,u,d,h=e.bodyFontSize,c=e.bodySpacing,f=e._bodyAlign,g=e.body,m=e.displayColors,p=0,v=m?Be(e,"left"):0,b=Ye(e.rtl,e.x,e.width),y=function(e){n.fillText(e,b.x(t.x+p),t.y+h/2),t.y+=h+c},x=b.textAlign(f);for(n.textAlign=f,n.textBaseline="middle",n.font=B.fontString(h,e._bodyFontStyle,e._bodyFontFamily),t.x=Be(e,x),n.fillStyle=e.bodyFontColor,B.each(e.beforeBody,y),p=m&&"right"!==x?"center"===f?h/2+1:h+2:0,s=0,u=g.length;s<u;++s){for(i=g[s],a=e.labelTextColors[s],r=e.labelColors[s],n.fillStyle=a,B.each(i.before,y),l=0,d=(o=i.lines).length;l<d;++l){if(m){var _=b.x(v);n.fillStyle=e.legendColorBackground,n.fillRect(b.leftForLtr(_,h),t.y,h,h),n.lineWidth=1,n.strokeStyle=r.borderColor,n.strokeRect(b.leftForLtr(_,h),t.y,h,h),n.fillStyle=r.backgroundColor,n.fillRect(b.leftForLtr(b.xPlus(_,1),h-2),t.y+1,h-2,h-2),n.fillStyle=a}y(o[l])}B.each(i.after,y)}p=0,B.each(e.afterBody,y),t.y-=c},drawFooter:function(t,e,n){var i,a,r=e.footer,o=r.length;if(o){var s=Ye(e.rtl,e.x,e.width);for(t.x=Be(e,e._footerAlign),t.y+=e.footerMarginTop,n.textAlign=s.textAlign(e._footerAlign),n.textBaseline="middle",i=e.footerFontSize,n.fillStyle=e.footerFontColor,n.font=B.fontString(i,e._footerFontStyle,e._footerFontFamily),a=0;a<o;++a)n.fillText(r[a],s.x(t.x),t.y+i/2),t.y+=i+e.footerSpacing}},drawBackground:function(t,e,n,i){n.fillStyle=e.backgroundColor,n.strokeStyle=e.borderColor,n.lineWidth=e.borderWidth;var a=e.xAlign,r=e.yAlign,o=t.x,s=t.y,l=i.width,u=i.height,d=e.cornerRadius;n.beginPath(),n.moveTo(o+d,s),"top"===r&&this.drawCaret(t,i),n.lineTo(o+l-d,s),n.quadraticCurveTo(o+l,s,o+l,s+d),"center"===r&&"right"===a&&this.drawCaret(t,i),n.lineTo(o+l,s+u-d),n.quadraticCurveTo(o+l,s+u,o+l-d,s+u),"bottom"===r&&this.drawCaret(t,i),n.lineTo(o+d,s+u),n.quadraticCurveTo(o,s+u,o,s+u-d),"center"===r&&"left"===a&&this.drawCaret(t,i),n.lineTo(o,s+d),n.quadraticCurveTo(o,s,o+d,s),n.closePath(),n.fill(),e.borderWidth>0&&n.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var n={width:e.width,height:e.height},i={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&r&&(t.save(),t.globalAlpha=a,this.drawBackground(i,e,t,n),i.y+=e.yPadding,B.rtl.overrideTextDirection(t,e.textDirection),this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),B.rtl.restoreTextDirection(t,e.textDirection),t.restore())}},handleEvent:function(t){var e,n=this,i=n._options;return n._lastActive=n._lastActive||[],"mouseout"===t.type?n._active=[]:(n._active=n._chart.getElementsAtEventForMode(t,i.mode,i),i.reverse&&n._active.reverse()),(e=!B.arrayEquals(n._active,n._lastActive))&&(n._lastActive=n._active,(i.enabled||i.custom)&&(n._eventPosition={x:t.x,y:t.y},n.update(!0),n.pivot())),e}}),Ge=ze,qe=Ue;qe.positioners=Ge;var Ze=B.valueOrDefault;function $e(){return B.merge(Object.create(null),[].slice.call(arguments),{merger:function(t,e,n,i){if("xAxes"===t||"yAxes"===t){var a,r,o,s=n[t].length;for(e[t]||(e[t]=[]),a=0;a<s;++a)o=n[t][a],r=Ze(o.type,"xAxes"===t?"category":"linear"),a>=e[t].length&&e[t].push({}),!e[t][a].type||o.type&&o.type!==e[t][a].type?B.merge(e[t][a],[Ne.getScaleDefaults(r),o]):B.merge(e[t][a],o)}else B._merger(t,e,n,i)}})}function Xe(){return B.merge(Object.create(null),[].slice.call(arguments),{merger:function(t,e,n,i){var a=e[t]||Object.create(null),r=n[t];"scales"===t?e[t]=$e(a,r):"scale"===t?e[t]=B.merge(a,[Ne.getScaleDefaults(r.type),r]):B._merger(t,e,n,i)}})}function Ke(t){var e=t.options;B.each(t.scales,(function(e){pe.removeBox(t,e)})),e=Xe(Y.global,Y[t.config.type],e),t.options=t.config.options=e,t.ensureScalesHaveIDs(),t.buildOrUpdateScales(),t.tooltip._options=e.tooltips,t.tooltip.initialize()}function Je(t,e,n){var i,a=function(t){return t.id===i};do{i=e+n++}while(B.findIndex(t,a)>=0);return i}function Qe(t){return"top"===t||"bottom"===t}function tn(t,e){return function(n,i){return n[t]===i[t]?n[e]-i[e]:n[t]-i[t]}}Y._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var en=function(t,e){return this.construct(t,e),this};B.extend(en.prototype,{construct:function(t,e){var n=this;e=function(t){var e=(t=t||Object.create(null)).data=t.data||{};return e.datasets=e.datasets||[],e.labels=e.labels||[],t.options=Xe(Y.global,Y[t.type],t.options||{}),t}(e);var i=Le.acquireContext(t,e),a=i&&i.canvas,r=a&&a.height,o=a&&a.width;n.id=B.uid(),n.ctx=i,n.canvas=a,n.config=e,n.width=o,n.height=r,n.aspectRatio=r?o/r:null,n.options=e.options,n._bufferedRender=!1,n._layers=[],n.chart=n,n.controller=n,en.instances[n.id]=n,Object.defineProperty(n,"data",{get:function(){return n.config.data},set:function(t){n.config.data=t}}),i&&a?(n.initialize(),n.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return Re.notify(t,"beforeInit"),B.retinaScale(t,t.options.devicePixelRatio),t.bindEvents(),t.options.responsive&&t.resize(!0),t.initToolTip(),Re.notify(t,"afterInit"),t},clear:function(){return B.canvas.clear(this),this},stop:function(){return Q.cancelAnimation(this),this},resize:function(t){var e=this,n=e.options,i=e.canvas,a=n.maintainAspectRatio&&e.aspectRatio||null,r=Math.max(0,Math.floor(B.getMaximumWidth(i))),o=Math.max(0,Math.floor(a?r/a:B.getMaximumHeight(i)));if((e.width!==r||e.height!==o)&&(i.width=e.width=r,i.height=e.height=o,i.style.width=r+"px",i.style.height=o+"px",B.retinaScale(e,n.devicePixelRatio),!t)){var s={width:r,height:o};Re.notify(e,"resize",[s]),n.onResize&&n.onResize(e,s),e.stop(),e.update({duration:n.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},n=t.scale;B.each(e.xAxes,(function(t,n){t.id||(t.id=Je(e.xAxes,"x-axis-",n))})),B.each(e.yAxes,(function(t,n){t.id||(t.id=Je(e.yAxes,"y-axis-",n))})),n&&(n.id=n.id||"scale")},buildOrUpdateScales:function(){var t=this,e=t.options,n=t.scales||{},i=[],a=Object.keys(n).reduce((function(t,e){return t[e]=!1,t}),{});e.scales&&(i=i.concat((e.scales.xAxes||[]).map((function(t){return{options:t,dtype:"category",dposition:"bottom"}})),(e.scales.yAxes||[]).map((function(t){return{options:t,dtype:"linear",dposition:"left"}})))),e.scale&&i.push({options:e.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),B.each(i,(function(e){var i=e.options,r=i.id,o=Ze(i.type,e.dtype);Qe(i.position)!==Qe(e.dposition)&&(i.position=e.dposition),a[r]=!0;var s=null;if(r in n&&n[r].type===o)(s=n[r]).options=i,s.ctx=t.ctx,s.chart=t;else{var l=Ne.getScaleConstructor(o);if(!l)return;s=new l({id:r,type:o,options:i,ctx:t.ctx,chart:t}),n[s.id]=s}s.mergeTicksOptions(),e.isDefault&&(t.scale=s)})),B.each(a,(function(t,e){t||delete n[e]})),t.scales=n,Ne.addScalesToLayout(this)},buildOrUpdateControllers:function(){var t,e,n=this,i=[],a=n.data.datasets;for(t=0,e=a.length;t<e;t++){var r=a[t],o=n.getDatasetMeta(t),s=r.type||n.config.type;if(o.type&&o.type!==s&&(n.destroyDatasetMeta(t),o=n.getDatasetMeta(t)),o.type=s,o.order=r.order||0,o.index=t,o.controller)o.controller.updateIndex(t),o.controller.linkScales();else{var l=Qt[o.type];if(void 0===l)throw new Error('"'+o.type+'" is not a chart type.');o.controller=new l(n,t),i.push(o.controller)}}return i},resetElements:function(){var t=this;B.each(t.data.datasets,(function(e,n){t.getDatasetMeta(n).controller.reset()}),t)},reset:function(){this.resetElements(),this.tooltip.initialize()},update:function(t){var e,n,i=this;if(t&&"object"==typeof t||(t={duration:t,lazy:arguments[1]}),Ke(i),Re._invalidate(i),!1!==Re.notify(i,"beforeUpdate")){i.tooltip._data=i.data;var a=i.buildOrUpdateControllers();for(e=0,n=i.data.datasets.length;e<n;e++)i.getDatasetMeta(e).controller.buildOrUpdateElements();i.updateLayout(),i.options.animation&&i.options.animation.duration&&B.each(a,(function(t){t.reset()})),i.updateDatasets(),i.tooltip.initialize(),i.lastActive=[],Re.notify(i,"afterUpdate"),i._layers.sort(tn("z","_idx")),i._bufferedRender?i._bufferedRequest={duration:t.duration,easing:t.easing,lazy:t.lazy}:i.render(t)}},updateLayout:function(){var t=this;!1!==Re.notify(t,"beforeLayout")&&(pe.update(this,this.width,this.height),t._layers=[],B.each(t.boxes,(function(e){e._configure&&e._configure(),t._layers.push.apply(t._layers,e._layers())}),t),t._layers.forEach((function(t,e){t._idx=e})),Re.notify(t,"afterScaleUpdate"),Re.notify(t,"afterLayout"))},updateDatasets:function(){if(!1!==Re.notify(this,"beforeDatasetsUpdate")){for(var t=0,e=this.data.datasets.length;t<e;++t)this.updateDataset(t);Re.notify(this,"afterDatasetsUpdate")}},updateDataset:function(t){var e=this.getDatasetMeta(t),n={meta:e,index:t};!1!==Re.notify(this,"beforeDatasetUpdate",[n])&&(e.controller._update(),Re.notify(this,"afterDatasetUpdate",[n]))},render:function(t){var e=this;t&&"object"==typeof t||(t={duration:t,lazy:arguments[1]});var n=e.options.animation,i=Ze(t.duration,n&&n.duration),a=t.lazy;if(!1!==Re.notify(e,"beforeRender")){var r=function(t){Re.notify(e,"afterRender"),B.callback(n&&n.onComplete,[t],e)};if(n&&i){var o=new J({numSteps:i/16.66,easing:t.easing||n.easing,render:function(t,e){var n=B.easing.effects[e.easing],i=e.currentStep,a=i/e.numSteps;t.draw(n(a),a,i)},onAnimationProgress:n.onProgress,onAnimationComplete:r});Q.addAnimation(e,o,i,a)}else e.draw(),r(new J({numSteps:0,chart:e}));return e}},draw:function(t){var e,n,i=this;if(i.clear(),B.isNullOrUndef(t)&&(t=1),i.transition(t),!(i.width<=0||i.height<=0)&&!1!==Re.notify(i,"beforeDraw",[t])){for(n=i._layers,e=0;e<n.length&&n[e].z<=0;++e)n[e].draw(i.chartArea);for(i.drawDatasets(t);e<n.length;++e)n[e].draw(i.chartArea);i._drawTooltip(t),Re.notify(i,"afterDraw",[t])}},transition:function(t){for(var e=0,n=(this.data.datasets||[]).length;e<n;++e)this.isDatasetVisible(e)&&this.getDatasetMeta(e).controller.transition(t);this.tooltip.transition(t)},_getSortedDatasetMetas:function(t){var e,n,i=[];for(e=0,n=(this.data.datasets||[]).length;e<n;++e)t&&!this.isDatasetVisible(e)||i.push(this.getDatasetMeta(e));return i.sort(tn("order","index")),i},_getSortedVisibleDatasetMetas:function(){return this._getSortedDatasetMetas(!0)},drawDatasets:function(t){var e,n;if(!1!==Re.notify(this,"beforeDatasetsDraw",[t])){for(n=(e=this._getSortedVisibleDatasetMetas()).length-1;n>=0;--n)this.drawDataset(e[n],t);Re.notify(this,"afterDatasetsDraw",[t])}},drawDataset:function(t,e){var n={meta:t,index:t.index,easingValue:e};!1!==Re.notify(this,"beforeDatasetDraw",[n])&&(t.controller.draw(e),Re.notify(this,"afterDatasetDraw",[n]))},_drawTooltip:function(t){var e=this.tooltip,n={tooltip:e,easingValue:t};!1!==Re.notify(this,"beforeTooltipDraw",[n])&&(e.draw(),Re.notify(this,"afterTooltipDraw",[n]))},getElementAtEvent:function(t){return oe.modes.single(this,t)},getElementsAtEvent:function(t){return oe.modes.label(this,t,{intersect:!0})},getElementsAtXAxis:function(t){return oe.modes["x-axis"](this,t,{intersect:!0})},getElementsAtEventForMode:function(t,e,n){var i=oe.modes[e];return"function"==typeof i?i(this,t,n):[]},getDatasetAtEvent:function(t){return oe.modes.dataset(this,t,{intersect:!0})},getDatasetMeta:function(t){var e=this.data.datasets[t];e._meta||(e._meta={});var n=e._meta[this.id];return n||(n=e._meta[this.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e.order||0,index:t}),n},getVisibleDatasetCount:function(){for(var t=0,e=0,n=this.data.datasets.length;e<n;++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)},destroyDatasetMeta:function(t){var e=this.id,n=this.data.datasets[t],i=n._meta&&n._meta[e];i&&(i.controller.destroy(),delete n._meta[e])},destroy:function(){var t,e,n=this,i=n.canvas;for(n.stop(),t=0,e=n.data.datasets.length;t<e;++t)n.destroyDatasetMeta(t);i&&(n.unbindEvents(),B.canvas.clear(n),Le.releaseContext(n.ctx),n.canvas=null,n.ctx=null),Re.notify(n,"destroy"),delete en.instances[n.id]},toBase64Image:function(){return this.canvas.toDataURL.apply(this.canvas,arguments)},initToolTip:function(){var t=this;t.tooltip=new qe({_chart:t,_chartInstance:t,_data:t.data,_options:t.options.tooltips},t)},bindEvents:function(){var t=this,e=t._listeners={},n=function(){t.eventHandler.apply(t,arguments)};B.each(t.options.events,(function(i){Le.addEventListener(t,i,n),e[i]=n})),t.options.responsive&&(n=function(){t.resize()},Le.addEventListener(t,"resize",n),e.resize=n)},unbindEvents:function(){var t=this,e=t._listeners;e&&(delete t._listeners,B.each(e,(function(e,n){Le.removeEventListener(t,n,e)})))},updateHoverStyle:function(t,e,n){var i,a,r,o=n?"set":"remove";for(a=0,r=t.length;a<r;++a)(i=t[a])&&this.getDatasetMeta(i._datasetIndex).controller[o+"HoverStyle"](i);"dataset"===e&&this.getDatasetMeta(t[0]._datasetIndex).controller["_"+o+"DatasetHoverStyle"]()},eventHandler:function(t){var e=this,n=e.tooltip;if(!1!==Re.notify(e,"beforeEvent",[t])){e._bufferedRender=!0,e._bufferedRequest=null;var i=e.handleEvent(t);n&&(i=n._start?n.handleEvent(t):i|n.handleEvent(t)),Re.notify(e,"afterEvent",[t]);var a=e._bufferedRequest;return a?e.render(a):i&&!e.animating&&(e.stop(),e.render({duration:e.options.hover.animationDuration,lazy:!0})),e._bufferedRender=!1,e._bufferedRequest=null,e}},handleEvent:function(t){var e,n=this,i=n.options||{},a=i.hover;return n.lastActive=n.lastActive||[],"mouseout"===t.type?n.active=[]:n.active=n.getElementsAtEventForMode(t,a.mode,a),B.callback(i.onHover||i.hover.onHover,[t.native,n.active],n),"mouseup"!==t.type&&"click"!==t.type||i.onClick&&i.onClick.call(n,t.native,n.active),n.lastActive.length&&n.updateHoverStyle(n.lastActive,a.mode,!1),n.active.length&&a.mode&&n.updateHoverStyle(n.active,a.mode,!0),e=!B.arrayEquals(n.active,n.lastActive),n.lastActive=n.active,e}}),en.instances={};var nn=en;en.Controller=en,en.types={},B.configMerge=Xe,B.scaleMerge=$e;function an(){throw new Error("This method is not implemented: either no adapter can be found or an incomplete integration was provided.")}function rn(t){this.options=t||{}}B.extend(rn.prototype,{formats:an,parse:an,format:an,add:an,diff:an,startOf:an,endOf:an,_create:function(t){return t}}),rn.override=function(t){B.extend(rn.prototype,t)};var on={_date:rn},sn={formatters:{values:function(t){return B.isArray(t)?t:""+t},linear:function(t,e,n){var i=n.length>3?n[2]-n[1]:n[1]-n[0];Math.abs(i)>1&&t!==Math.floor(t)&&(i=t-Math.floor(t));var a=B.log10(Math.abs(i)),r="";if(0!==t)if(Math.max(Math.abs(n[0]),Math.abs(n[n.length-1]))<1e-4){var o=B.log10(Math.abs(t)),s=Math.floor(o)-Math.floor(a);s=Math.max(Math.min(s,20),0),r=t.toExponential(s)}else{var l=-1*Math.floor(a);l=Math.max(Math.min(l,20),0),r=t.toFixed(l)}else r="0";return r},logarithmic:function(t,e,n){var i=t/Math.pow(10,Math.floor(B.log10(t)));return 0===t?"0":1===i||2===i||5===i||0===e||e===n.length-1?t.toExponential():""}}},ln=B.isArray,un=B.isNullOrUndef,dn=B.valueOrDefault,hn=B.valueAtIndexOrDefault;function cn(t,e,n){var i,a=t.getTicks().length,r=Math.min(e,a-1),o=t.getPixelForTick(r),s=t._startPixel,l=t._endPixel;if(!(n&&(i=1===a?Math.max(o-s,l-o):0===e?(t.getPixelForTick(1)-o)/2:(o-t.getPixelForTick(r-1))/2,(o+=r<e?i:-i)<s-1e-6||o>l+1e-6)))return o}function fn(t,e,n,i){var a,r,o,s,l,u,d,h,c,f,g,m,p,v=n.length,b=[],y=[],x=[],_=0,w=0;for(a=0;a<v;++a){if(s=n[a].label,l=n[a].major?e.major:e.minor,t.font=u=l.string,d=i[u]=i[u]||{data:{},gc:[]},h=l.lineHeight,c=f=0,un(s)||ln(s)){if(ln(s))for(r=0,o=s.length;r<o;++r)g=s[r],un(g)||ln(g)||(c=B.measureText(t,d.data,d.gc,c,g),f+=h)}else c=B.measureText(t,d.data,d.gc,c,s),f=h;b.push(c),y.push(f),x.push(h/2),_=Math.max(c,_),w=Math.max(f,w)}function k(t){return{width:b[t]||0,height:y[t]||0,offset:x[t]||0}}return function(t,e){B.each(t,(function(t){var n,i=t.gc,a=i.length/2;if(a>e){for(n=0;n<a;++n)delete t.data[i[n]];i.splice(0,a)}}))}(i,v),m=b.indexOf(_),p=y.indexOf(w),{first:k(0),last:k(v-1),widest:k(m),highest:k(p)}}function gn(t){return t.drawTicks?t.tickMarkLength:0}function mn(t){var e,n;return t.display?(e=B.options._parseFont(t),n=B.options.toPadding(t.padding),e.lineHeight+n.height):0}function pn(t,e){return B.extend(B.options._parseFont({fontFamily:dn(e.fontFamily,t.fontFamily),fontSize:dn(e.fontSize,t.fontSize),fontStyle:dn(e.fontStyle,t.fontStyle),lineHeight:dn(e.lineHeight,t.lineHeight)}),{color:B.options.resolve([e.fontColor,t.fontColor,Y.global.defaultFontColor])})}function vn(t){var e=pn(t,t.minor);return{minor:e,major:t.major.enabled?pn(t,t.major):e}}function bn(t){var e,n,i,a=[];for(n=0,i=t.length;n<i;++n)void 0!==(e=t[n])._index&&a.push(e);return a}function yn(t,e,n,i){var a,r,o,s,l=dn(n,0),u=Math.min(dn(i,t.length),t.length),d=0;for(e=Math.ceil(e),i&&(e=(a=i-n)/Math.floor(a/e)),s=l;s<0;)d++,s=Math.round(l+d*e);for(r=Math.max(l,0);r<u;r++)o=t[r],r===s?(o._index=r,d++,s=Math.round(l+d*e)):delete o.label}Y._set("scale",{display:!0,position:"left",offset:!1,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)",zeroLineBorderDash:[],zeroLineBorderDashOffset:0,offsetGridLines:!1,borderDash:[],borderDashOffset:0},scaleLabel:{display:!1,labelString:"",padding:{top:4,bottom:4}},ticks:{beginAtZero:!1,minRotation:0,maxRotation:50,mirror:!1,padding:0,reverse:!1,display:!0,autoSkip:!0,autoSkipPadding:0,labelOffset:0,callback:sn.formatters.values,minor:{},major:{}}});var xn=X.extend({zeroLineIndex:0,getPadding:function(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}},getTicks:function(){return this._ticks},_getLabels:function(){var t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]},mergeTicksOptions:function(){},beforeUpdate:function(){B.callback(this.options.beforeUpdate,[this])},update:function(t,e,n){var i,a,r,o,s,l=this,u=l.options.ticks,d=u.sampleSize;if(l.beforeUpdate(),l.maxWidth=t,l.maxHeight=e,l.margins=B.extend({left:0,right:0,top:0,bottom:0},n),l._ticks=null,l.ticks=null,l._labelSizes=null,l._maxLabelLines=0,l.longestLabelWidth=0,l.longestTextCache=l.longestTextCache||{},l._gridLineItems=null,l._labelItems=null,l.beforeSetDimensions(),l.setDimensions(),l.afterSetDimensions(),l.beforeDataLimits(),l.determineDataLimits(),l.afterDataLimits(),l.beforeBuildTicks(),o=l.buildTicks()||[],(!(o=l.afterBuildTicks(o)||o)||!o.length)&&l.ticks)for(o=[],i=0,a=l.ticks.length;i<a;++i)o.push({value:l.ticks[i],major:!1});return l._ticks=o,s=d<o.length,r=l._convertTicksToLabels(s?function(t,e){for(var n=[],i=t.length/e,a=0,r=t.length;a<r;a+=i)n.push(t[Math.floor(a)]);return n}(o,d):o),l._configure(),l.beforeCalculateTickRotation(),l.calculateTickRotation(),l.afterCalculateTickRotation(),l.beforeFit(),l.fit(),l.afterFit(),l._ticksToDraw=u.display&&(u.autoSkip||"auto"===u.source)?l._autoSkip(o):o,s&&(r=l._convertTicksToLabels(l._ticksToDraw)),l.ticks=r,l.afterUpdate(),l.minSize},_configure:function(){var t,e,n=this,i=n.options.ticks.reverse;n.isHorizontal()?(t=n.left,e=n.right):(t=n.top,e=n.bottom,i=!i),n._startPixel=t,n._endPixel=e,n._reversePixels=i,n._length=e-t},afterUpdate:function(){B.callback(this.options.afterUpdate,[this])},beforeSetDimensions:function(){B.callback(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(){B.callback(this.options.afterSetDimensions,[this])},beforeDataLimits:function(){B.callback(this.options.beforeDataLimits,[this])},determineDataLimits:B.noop,afterDataLimits:function(){B.callback(this.options.afterDataLimits,[this])},beforeBuildTicks:function(){B.callback(this.options.beforeBuildTicks,[this])},buildTicks:B.noop,afterBuildTicks:function(t){var e=this;return ln(t)&&t.length?B.callback(e.options.afterBuildTicks,[e,t]):(e.ticks=B.callback(e.options.afterBuildTicks,[e,e.ticks])||e.ticks,t)},beforeTickToLabelConversion:function(){B.callback(this.options.beforeTickToLabelConversion,[this])},convertTicksToLabels:function(){var t=this.options.ticks;this.ticks=this.ticks.map(t.userCallback||t.callback,this)},afterTickToLabelConversion:function(){B.callback(this.options.afterTickToLabelConversion,[this])},beforeCalculateTickRotation:function(){B.callback(this.options.beforeCalculateTickRotation,[this])},calculateTickRotation:function(){var t,e,n,i,a,r,o,s=this,l=s.options,u=l.ticks,d=s.getTicks().length,h=u.minRotation||0,c=u.maxRotation,f=h;!s._isVisible()||!u.display||h>=c||d<=1||!s.isHorizontal()?s.labelRotation=h:(e=(t=s._getLabelSizes()).widest.width,n=t.highest.height-t.highest.offset,i=Math.min(s.maxWidth,s.chart.width-e),e+6>(a=l.offset?s.maxWidth/d:i/(d-1))&&(a=i/(d-(l.offset?.5:1)),r=s.maxHeight-gn(l.gridLines)-u.padding-mn(l.scaleLabel),o=Math.sqrt(e*e+n*n),f=B.toDegrees(Math.min(Math.asin(Math.min((t.highest.height+6)/a,1)),Math.asin(Math.min(r/o,1))-Math.asin(n/o))),f=Math.max(h,Math.min(c,f))),s.labelRotation=f)},afterCalculateTickRotation:function(){B.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){B.callback(this.options.beforeFit,[this])},fit:function(){var t=this,e=t.minSize={width:0,height:0},n=t.chart,i=t.options,a=i.ticks,r=i.scaleLabel,o=i.gridLines,s=t._isVisible(),l="bottom"===i.position,u=t.isHorizontal();if(u?e.width=t.maxWidth:s&&(e.width=gn(o)+mn(r)),u?s&&(e.height=gn(o)+mn(r)):e.height=t.maxHeight,a.display&&s){var d=vn(a),h=t._getLabelSizes(),c=h.first,f=h.last,g=h.widest,m=h.highest,p=.4*d.minor.lineHeight,v=a.padding;if(u){var b=0!==t.labelRotation,y=B.toRadians(t.labelRotation),x=Math.cos(y),_=Math.sin(y),w=_*g.width+x*(m.height-(b?m.offset:0))+(b?0:p);e.height=Math.min(t.maxHeight,e.height+w+v);var k,M,S=t.getPixelForTick(0)-t.left,D=t.right-t.getPixelForTick(t.getTicks().length-1);b?(k=l?x*c.width+_*c.offset:_*(c.height-c.offset),M=l?_*(f.height-f.offset):x*f.width+_*f.offset):(k=c.width/2,M=f.width/2),t.paddingLeft=Math.max((k-S)*t.width/(t.width-S),0)+3,t.paddingRight=Math.max((M-D)*t.width/(t.width-D),0)+3}else{var C=a.mirror?0:g.width+v+p;e.width=Math.min(t.maxWidth,e.width+C),t.paddingTop=c.height/2,t.paddingBottom=f.height/2}}t.handleMargins(),u?(t.width=t._length=n.width-t.margins.left-t.margins.right,t.height=e.height):(t.width=e.width,t.height=t._length=n.height-t.margins.top-t.margins.bottom)},handleMargins:function(){var t=this;t.margins&&(t.margins.left=Math.max(t.paddingLeft,t.margins.left),t.margins.top=Math.max(t.paddingTop,t.margins.top),t.margins.right=Math.max(t.paddingRight,t.margins.right),t.margins.bottom=Math.max(t.paddingBottom,t.margins.bottom))},afterFit:function(){B.callback(this.options.afterFit,[this])},isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(t){if(un(t))return NaN;if(("number"==typeof t||t instanceof Number)&&!isFinite(t))return NaN;if(t)if(this.isHorizontal()){if(void 0!==t.x)return this.getRightValue(t.x)}else if(void 0!==t.y)return this.getRightValue(t.y);return t},_convertTicksToLabels:function(t){var e,n,i,a=this;for(a.ticks=t.map((function(t){return t.value})),a.beforeTickToLabelConversion(),e=a.convertTicksToLabels(t)||a.ticks,a.afterTickToLabelConversion(),n=0,i=t.length;n<i;++n)t[n].label=e[n];return e},_getLabelSizes:function(){var t=this,e=t._labelSizes;return e||(t._labelSizes=e=fn(t.ctx,vn(t.options.ticks),t.getTicks(),t.longestTextCache),t.longestLabelWidth=e.widest.width),e},_parseValue:function(t){var e,n,i,a;return ln(t)?(e=+this.getRightValue(t[0]),n=+this.getRightValue(t[1]),i=Math.min(e,n),a=Math.max(e,n)):(e=void 0,n=t=+this.getRightValue(t),i=t,a=t),{min:i,max:a,start:e,end:n}},_getScaleLabel:function(t){var e=this._parseValue(t);return void 0!==e.start?"["+e.start+", "+e.end+"]":+this.getRightValue(t)},getLabelForIndex:B.noop,getPixelForValue:B.noop,getValueForPixel:B.noop,getPixelForTick:function(t){var e=this.options.offset,n=this._ticks.length,i=1/Math.max(n-(e?0:1),1);return t<0||t>n-1?null:this.getPixelForDecimal(t*i+(e?i/2:0))},getPixelForDecimal:function(t){return this._reversePixels&&(t=1-t),this._startPixel+t*this._length},getDecimalForPixel:function(t){var e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var t=this.min,e=this.max;return this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0},_autoSkip:function(t){var e,n,i,a,r=this.options.ticks,o=this._length,s=r.maxTicksLimit||o/this._tickSize()+1,l=r.major.enabled?function(t){var e,n,i=[];for(e=0,n=t.length;e<n;e++)t[e].major&&i.push(e);return i}(t):[],u=l.length,d=l[0],h=l[u-1];if(u>s)return function(t,e,n){var i,a,r=0,o=e[0];for(n=Math.ceil(n),i=0;i<t.length;i++)a=t[i],i===o?(a._index=i,o=e[++r*n]):delete a.label}(t,l,u/s),bn(t);if(i=function(t,e,n,i){var a,r,o,s,l=function(t){var e,n,i=t.length;if(i<2)return!1;for(n=t[0],e=1;e<i;++e)if(t[e]-t[e-1]!==n)return!1;return n}(t),u=(e.length-1)/i;if(!l)return Math.max(u,1);for(o=0,s=(a=B.math._factorize(l)).length-1;o<s;o++)if((r=a[o])>u)return r;return Math.max(u,1)}(l,t,0,s),u>0){for(e=0,n=u-1;e<n;e++)yn(t,i,l[e],l[e+1]);return a=u>1?(h-d)/(u-1):null,yn(t,i,B.isNullOrUndef(a)?0:d-a,d),yn(t,i,h,B.isNullOrUndef(a)?t.length:h+a),bn(t)}return yn(t,i),bn(t)},_tickSize:function(){var t=this.options.ticks,e=B.toRadians(this.labelRotation),n=Math.abs(Math.cos(e)),i=Math.abs(Math.sin(e)),a=this._getLabelSizes(),r=t.autoSkipPadding||0,o=a?a.widest.width+r:0,s=a?a.highest.height+r:0;return this.isHorizontal()?s*n>o*i?o/n:s/i:s*i<o*n?s/n:o/i},_isVisible:function(){var t,e,n,i=this.chart,a=this.options.display;if("auto"!==a)return!!a;for(t=0,e=i.data.datasets.length;t<e;++t)if(i.isDatasetVisible(t)&&((n=i.getDatasetMeta(t)).xAxisID===this.id||n.yAxisID===this.id))return!0;return!1},_computeGridLineItems:function(t){var e,n,i,a,r,o,s,l,u,d,h,c,f,g,m,p,v,b=this,y=b.chart,x=b.options,_=x.gridLines,w=x.position,k=_.offsetGridLines,M=b.isHorizontal(),S=b._ticksToDraw,D=S.length+(k?1:0),C=gn(_),P=[],T=_.drawBorder?hn(_.lineWidth,0,0):0,O=T/2,A=B._alignPixel,F=function(t){return A(y,t,T)};for("top"===w?(e=F(b.bottom),s=b.bottom-C,u=e-O,h=F(t.top)+O,f=t.bottom):"bottom"===w?(e=F(b.top),h=t.top,f=F(t.bottom)-O,s=e+O,u=b.top+C):"left"===w?(e=F(b.right),o=b.right-C,l=e-O,d=F(t.left)+O,c=t.right):(e=F(b.left),d=t.left,c=F(t.right)-O,o=e+O,l=b.left+C),n=0;n<D;++n)i=S[n]||{},un(i.label)&&n<S.length||(n===b.zeroLineIndex&&x.offset===k?(g=_.zeroLineWidth,m=_.zeroLineColor,p=_.zeroLineBorderDash||[],v=_.zeroLineBorderDashOffset||0):(g=hn(_.lineWidth,n,1),m=hn(_.color,n,"rgba(0,0,0,0.1)"),p=_.borderDash||[],v=_.borderDashOffset||0),void 0!==(a=cn(b,i._index||n,k))&&(r=A(y,a,g),M?o=l=d=c=r:s=u=h=f=r,P.push({tx1:o,ty1:s,tx2:l,ty2:u,x1:d,y1:h,x2:c,y2:f,width:g,color:m,borderDash:p,borderDashOffset:v})));return P.ticksLength=D,P.borderValue=e,P},_computeLabelItems:function(){var t,e,n,i,a,r,o,s,l,u,d,h,c=this,f=c.options,g=f.ticks,m=f.position,p=g.mirror,v=c.isHorizontal(),b=c._ticksToDraw,y=vn(g),x=g.padding,_=gn(f.gridLines),w=-B.toRadians(c.labelRotation),k=[];for("top"===m?(r=c.bottom-_-x,o=w?"left":"center"):"bottom"===m?(r=c.top+_+x,o=w?"right":"center"):"left"===m?(a=c.right-(p?0:_)-x,o=p?"left":"right"):(a=c.left+(p?0:_)+x,o=p?"right":"left"),t=0,e=b.length;t<e;++t)i=(n=b[t]).label,un(i)||(s=c.getPixelForTick(n._index||t)+g.labelOffset,u=(l=n.major?y.major:y.minor).lineHeight,d=ln(i)?i.length:1,v?(a=s,h="top"===m?((w?1:.5)-d)*u:(w?0:.5)*u):(r=s,h=(1-d)*u/2),k.push({x:a,y:r,rotation:w,label:i,font:l,textOffset:h,textAlign:o}));return k},_drawGrid:function(t){var e=this,n=e.options.gridLines;if(n.display){var i,a,r,o,s,l=e.ctx,u=e.chart,d=B._alignPixel,h=n.drawBorder?hn(n.lineWidth,0,0):0,c=e._gridLineItems||(e._gridLineItems=e._computeGridLineItems(t));for(r=0,o=c.length;r<o;++r)i=(s=c[r]).width,a=s.color,i&&a&&(l.save(),l.lineWidth=i,l.strokeStyle=a,l.setLineDash&&(l.setLineDash(s.borderDash),l.lineDashOffset=s.borderDashOffset),l.beginPath(),n.drawTicks&&(l.moveTo(s.tx1,s.ty1),l.lineTo(s.tx2,s.ty2)),n.drawOnChartArea&&(l.moveTo(s.x1,s.y1),l.lineTo(s.x2,s.y2)),l.stroke(),l.restore());if(h){var f,g,m,p,v=h,b=hn(n.lineWidth,c.ticksLength-1,1),y=c.borderValue;e.isHorizontal()?(f=d(u,e.left,v)-v/2,g=d(u,e.right,b)+b/2,m=p=y):(m=d(u,e.top,v)-v/2,p=d(u,e.bottom,b)+b/2,f=g=y),l.lineWidth=h,l.strokeStyle=hn(n.color,0),l.beginPath(),l.moveTo(f,m),l.lineTo(g,p),l.stroke()}}},_drawLabels:function(){var t=this;if(t.options.ticks.display){var e,n,i,a,r,o,s,l,u=t.ctx,d=t._labelItems||(t._labelItems=t._computeLabelItems());for(e=0,i=d.length;e<i;++e){if(o=(r=d[e]).font,u.save(),u.translate(r.x,r.y),u.rotate(r.rotation),u.font=o.string,u.fillStyle=o.color,u.textBaseline="middle",u.textAlign=r.textAlign,s=r.label,l=r.textOffset,ln(s))for(n=0,a=s.length;n<a;++n)u.fillText(""+s[n],0,l),l+=o.lineHeight;else u.fillText(s,0,l);u.restore()}}},_drawTitle:function(){var t=this,e=t.ctx,n=t.options,i=n.scaleLabel;if(i.display){var a,r,o=dn(i.fontColor,Y.global.defaultFontColor),s=B.options._parseFont(i),l=B.options.toPadding(i.padding),u=s.lineHeight/2,d=n.position,h=0;if(t.isHorizontal())a=t.left+t.width/2,r="bottom"===d?t.bottom-u-l.bottom:t.top+u+l.top;else{var c="left"===d;a=c?t.left+u+l.top:t.right-u-l.top,r=t.top+t.height/2,h=c?-.5*Math.PI:.5*Math.PI}e.save(),e.translate(a,r),e.rotate(h),e.textAlign="center",e.textBaseline="middle",e.fillStyle=o,e.font=s.string,e.fillText(i.labelString,0,0),e.restore()}},draw:function(t){this._isVisible()&&(this._drawGrid(t),this._drawTitle(),this._drawLabels())},_layers:function(){var t=this,e=t.options,n=e.ticks&&e.ticks.z||0,i=e.gridLines&&e.gridLines.z||0;return t._isVisible()&&n!==i&&t.draw===t._draw?[{z:i,draw:function(){t._drawGrid.apply(t,arguments),t._drawTitle.apply(t,arguments)}},{z:n,draw:function(){t._drawLabels.apply(t,arguments)}}]:[{z:n,draw:function(){t.draw.apply(t,arguments)}}]},_getMatchingVisibleMetas:function(t){var e=this,n=e.isHorizontal();return e.chart._getSortedVisibleDatasetMetas().filter((function(i){return(!t||i.type===t)&&(n?i.xAxisID===e.id:i.yAxisID===e.id)}))}});xn.prototype._draw=xn.prototype.draw;var _n=xn,wn=B.isNullOrUndef,kn=_n.extend({determineDataLimits:function(){var t,e=this,n=e._getLabels(),i=e.options.ticks,a=i.min,r=i.max,o=0,s=n.length-1;void 0!==a&&(t=n.indexOf(a))>=0&&(o=t),void 0!==r&&(t=n.indexOf(r))>=0&&(s=t),e.minIndex=o,e.maxIndex=s,e.min=n[o],e.max=n[s]},buildTicks:function(){var t=this._getLabels(),e=this.minIndex,n=this.maxIndex;this.ticks=0===e&&n===t.length-1?t:t.slice(e,n+1)},getLabelForIndex:function(t,e){var n=this.chart;return n.getDatasetMeta(e).controller._getValueScaleId()===this.id?this.getRightValue(n.data.datasets[e].data[t]):this._getLabels()[t]},_configure:function(){var t=this,e=t.options.offset,n=t.ticks;_n.prototype._configure.call(t),t.isHorizontal()||(t._reversePixels=!t._reversePixels),n&&(t._startValue=t.minIndex-(e?.5:0),t._valueRange=Math.max(n.length-(e?0:1),1))},getPixelForValue:function(t,e,n){var i,a,r,o=this;return wn(e)||wn(n)||(t=o.chart.data.datasets[n].data[e]),wn(t)||(i=o.isHorizontal()?t.x:t.y),(void 0!==i||void 0!==t&&isNaN(e))&&(a=o._getLabels(),t=B.valueOrDefault(i,t),e=-1!==(r=a.indexOf(t))?r:e,isNaN(e)&&(e=t)),o.getPixelForDecimal((e-o._startValue)/o._valueRange)},getPixelForTick:function(t){var e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t],t+this.minIndex)},getValueForPixel:function(t){var e=Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange);return Math.min(Math.max(e,0),this.ticks.length-1)},getBasePixel:function(){return this.bottom}}),Mn={position:"bottom"};kn._defaults=Mn;var Sn=B.noop,Dn=B.isNullOrUndef;var Cn=_n.extend({getRightValue:function(t){return"string"==typeof t?+t:_n.prototype.getRightValue.call(this,t)},handleTickRangeOptions:function(){var t=this,e=t.options.ticks;if(e.beginAtZero){var n=B.sign(t.min),i=B.sign(t.max);n<0&&i<0?t.max=0:n>0&&i>0&&(t.min=0)}var a=void 0!==e.min||void 0!==e.suggestedMin,r=void 0!==e.max||void 0!==e.suggestedMax;void 0!==e.min?t.min=e.min:void 0!==e.suggestedMin&&(null===t.min?t.min=e.suggestedMin:t.min=Math.min(t.min,e.suggestedMin)),void 0!==e.max?t.max=e.max:void 0!==e.suggestedMax&&(null===t.max?t.max=e.suggestedMax:t.max=Math.max(t.max,e.suggestedMax)),a!==r&&t.min>=t.max&&(a?t.max=t.min+1:t.min=t.max-1),t.min===t.max&&(t.max++,e.beginAtZero||t.min--)},getTickLimit:function(){var t,e=this.options.ticks,n=e.stepSize,i=e.maxTicksLimit;return n?t=Math.ceil(this.max/n)-Math.floor(this.min/n)+1:(t=this._computeTickLimit(),i=i||11),i&&(t=Math.min(i,t)),t},_computeTickLimit:function(){return Number.POSITIVE_INFINITY},handleDirectionalChanges:Sn,buildTicks:function(){var t=this,e=t.options.ticks,n=t.getTickLimit(),i={maxTicks:n=Math.max(2,n),min:e.min,max:e.max,precision:e.precision,stepSize:B.valueOrDefault(e.fixedStepSize,e.stepSize)},a=t.ticks=function(t,e){var n,i,a,r,o=[],s=t.stepSize,l=s||1,u=t.maxTicks-1,d=t.min,h=t.max,c=t.precision,f=e.min,g=e.max,m=B.niceNum((g-f)/u/l)*l;if(m<1e-14&&Dn(d)&&Dn(h))return[f,g];(r=Math.ceil(g/m)-Math.floor(f/m))>u&&(m=B.niceNum(r*m/u/l)*l),s||Dn(c)?n=Math.pow(10,B._decimalPlaces(m)):(n=Math.pow(10,c),m=Math.ceil(m*n)/n),i=Math.floor(f/m)*m,a=Math.ceil(g/m)*m,s&&(!Dn(d)&&B.almostWhole(d/m,m/1e3)&&(i=d),!Dn(h)&&B.almostWhole(h/m,m/1e3)&&(a=h)),r=(a-i)/m,r=B.almostEquals(r,Math.round(r),m/1e3)?Math.round(r):Math.ceil(r),i=Math.round(i*n)/n,a=Math.round(a*n)/n,o.push(Dn(d)?i:d);for(var p=1;p<r;++p)o.push(Math.round((i+p*m)*n)/n);return o.push(Dn(h)?a:h),o}(i,t);t.handleDirectionalChanges(),t.max=B.max(a),t.min=B.min(a),e.reverse?(a.reverse(),t.start=t.max,t.end=t.min):(t.start=t.min,t.end=t.max)},convertTicksToLabels:function(){var t=this;t.ticksAsNumbers=t.ticks.slice(),t.zeroLineIndex=t.ticks.indexOf(0),_n.prototype.convertTicksToLabels.call(t)},_configure:function(){var t,e=this,n=e.getTicks(),i=e.min,a=e.max;_n.prototype._configure.call(e),e.options.offset&&n.length&&(i-=t=(a-i)/Math.max(n.length-1,1)/2,a+=t),e._startValue=i,e._endValue=a,e._valueRange=a-i}}),Pn={position:"left",ticks:{callback:sn.formatters.linear}};function Tn(t,e,n,i){var a,r,o=t.options,s=function(t,e,n){var i=[n.type,void 0===e&&void 0===n.stack?n.index:"",n.stack].join(".");return void 0===t[i]&&(t[i]={pos:[],neg:[]}),t[i]}(e,o.stacked,n),l=s.pos,u=s.neg,d=i.length;for(a=0;a<d;++a)r=t._parseValue(i[a]),isNaN(r.min)||isNaN(r.max)||n.data[a].hidden||(l[a]=l[a]||0,u[a]=u[a]||0,o.relativePoints?l[a]=100:r.min<0||r.max<0?u[a]+=r.min:l[a]+=r.max)}function On(t,e,n){var i,a,r=n.length;for(i=0;i<r;++i)a=t._parseValue(n[i]),isNaN(a.min)||isNaN(a.max)||e.data[i].hidden||(t.min=Math.min(t.min,a.min),t.max=Math.max(t.max,a.max))}var An=Cn.extend({determineDataLimits:function(){var t,e,n,i,a=this,r=a.options,o=a.chart.data.datasets,s=a._getMatchingVisibleMetas(),l=r.stacked,u={},d=s.length;if(a.min=Number.POSITIVE_INFINITY,a.max=Number.NEGATIVE_INFINITY,void 0===l)for(t=0;!l&&t<d;++t)l=void 0!==(e=s[t]).stack;for(t=0;t<d;++t)n=o[(e=s[t]).index].data,l?Tn(a,u,e,n):On(a,e,n);B.each(u,(function(t){i=t.pos.concat(t.neg),a.min=Math.min(a.min,B.min(i)),a.max=Math.max(a.max,B.max(i))})),a.min=B.isFinite(a.min)&&!isNaN(a.min)?a.min:0,a.max=B.isFinite(a.max)&&!isNaN(a.max)?a.max:1,a.handleTickRangeOptions()},_computeTickLimit:function(){var t;return this.isHorizontal()?Math.ceil(this.width/40):(t=B.options._parseFont(this.options.ticks),Math.ceil(this.height/t.lineHeight))},handleDirectionalChanges:function(){this.isHorizontal()||this.ticks.reverse()},getLabelForIndex:function(t,e){return this._getScaleLabel(this.chart.data.datasets[e].data[t])},getPixelForValue:function(t){return this.getPixelForDecimal((+this.getRightValue(t)-this._startValue)/this._valueRange)},getValueForPixel:function(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange},getPixelForTick:function(t){var e=this.ticksAsNumbers;return t<0||t>e.length-1?null:this.getPixelForValue(e[t])}}),Fn=Pn;An._defaults=Fn;var In=B.valueOrDefault,Ln=B.math.log10;var Rn={position:"left",ticks:{callback:sn.formatters.logarithmic}};function Nn(t,e){return B.isFinite(t)&&t>=0?t:e}var Wn=_n.extend({determineDataLimits:function(){var t,e,n,i,a,r,o=this,s=o.options,l=o.chart,u=l.data.datasets,d=o.isHorizontal();function h(t){return d?t.xAxisID===o.id:t.yAxisID===o.id}o.min=Number.POSITIVE_INFINITY,o.max=Number.NEGATIVE_INFINITY,o.minNotZero=Number.POSITIVE_INFINITY;var c=s.stacked;if(void 0===c)for(t=0;t<u.length;t++)if(e=l.getDatasetMeta(t),l.isDatasetVisible(t)&&h(e)&&void 0!==e.stack){c=!0;break}if(s.stacked||c){var f={};for(t=0;t<u.length;t++){var g=[(e=l.getDatasetMeta(t)).type,void 0===s.stacked&&void 0===e.stack?t:"",e.stack].join(".");if(l.isDatasetVisible(t)&&h(e))for(void 0===f[g]&&(f[g]=[]),a=0,r=(i=u[t].data).length;a<r;a++){var m=f[g];n=o._parseValue(i[a]),isNaN(n.min)||isNaN(n.max)||e.data[a].hidden||n.min<0||n.max<0||(m[a]=m[a]||0,m[a]+=n.max)}}B.each(f,(function(t){if(t.length>0){var e=B.min(t),n=B.max(t);o.min=Math.min(o.min,e),o.max=Math.max(o.max,n)}}))}else for(t=0;t<u.length;t++)if(e=l.getDatasetMeta(t),l.isDatasetVisible(t)&&h(e))for(a=0,r=(i=u[t].data).length;a<r;a++)n=o._parseValue(i[a]),isNaN(n.min)||isNaN(n.max)||e.data[a].hidden||n.min<0||n.max<0||(o.min=Math.min(n.min,o.min),o.max=Math.max(n.max,o.max),0!==n.min&&(o.minNotZero=Math.min(n.min,o.minNotZero)));o.min=B.isFinite(o.min)?o.min:null,o.max=B.isFinite(o.max)?o.max:null,o.minNotZero=B.isFinite(o.minNotZero)?o.minNotZero:null,this.handleTickRangeOptions()},handleTickRangeOptions:function(){var t=this,e=t.options.ticks;t.min=Nn(e.min,t.min),t.max=Nn(e.max,t.max),t.min===t.max&&(0!==t.min&&null!==t.min?(t.min=Math.pow(10,Math.floor(Ln(t.min))-1),t.max=Math.pow(10,Math.floor(Ln(t.max))+1)):(t.min=1,t.max=10)),null===t.min&&(t.min=Math.pow(10,Math.floor(Ln(t.max))-1)),null===t.max&&(t.max=0!==t.min?Math.pow(10,Math.floor(Ln(t.min))+1):10),null===t.minNotZero&&(t.min>0?t.minNotZero=t.min:t.max<1?t.minNotZero=Math.pow(10,Math.floor(Ln(t.max))):t.minNotZero=1)},buildTicks:function(){var t=this,e=t.options.ticks,n=!t.isHorizontal(),i={min:Nn(e.min),max:Nn(e.max)},a=t.ticks=function(t,e){var n,i,a=[],r=In(t.min,Math.pow(10,Math.floor(Ln(e.min)))),o=Math.floor(Ln(e.max)),s=Math.ceil(e.max/Math.pow(10,o));0===r?(n=Math.floor(Ln(e.minNotZero)),i=Math.floor(e.minNotZero/Math.pow(10,n)),a.push(r),r=i*Math.pow(10,n)):(n=Math.floor(Ln(r)),i=Math.floor(r/Math.pow(10,n)));var l=n<0?Math.pow(10,Math.abs(n)):1;do{a.push(r),10===++i&&(i=1,l=++n>=0?1:l),r=Math.round(i*Math.pow(10,n)*l)/l}while(n<o||n===o&&i<s);var u=In(t.max,r);return a.push(u),a}(i,t);t.max=B.max(a),t.min=B.min(a),e.reverse?(n=!n,t.start=t.max,t.end=t.min):(t.start=t.min,t.end=t.max),n&&a.reverse()},convertTicksToLabels:function(){this.tickValues=this.ticks.slice(),_n.prototype.convertTicksToLabels.call(this)},getLabelForIndex:function(t,e){return this._getScaleLabel(this.chart.data.datasets[e].data[t])},getPixelForTick:function(t){var e=this.tickValues;return t<0||t>e.length-1?null:this.getPixelForValue(e[t])},_getFirstTickValue:function(t){var e=Math.floor(Ln(t));return Math.floor(t/Math.pow(10,e))*Math.pow(10,e)},_configure:function(){var t=this,e=t.min,n=0;_n.prototype._configure.call(t),0===e&&(e=t._getFirstTickValue(t.minNotZero),n=In(t.options.ticks.fontSize,Y.global.defaultFontSize)/t._length),t._startValue=Ln(e),t._valueOffset=n,t._valueRange=(Ln(t.max)-Ln(e))/(1-n)},getPixelForValue:function(t){var e=this,n=0;return(t=+e.getRightValue(t))>e.min&&t>0&&(n=(Ln(t)-e._startValue)/e._valueRange+e._valueOffset),e.getPixelForDecimal(n)},getValueForPixel:function(t){var e=this,n=e.getDecimalForPixel(t);return 0===n&&0===e.min?0:Math.pow(10,e._startValue+(n-e._valueOffset)*e._valueRange)}}),Yn=Rn;Wn._defaults=Yn;var zn=B.valueOrDefault,En=B.valueAtIndexOrDefault,Vn=B.options.resolve,Hn={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,color:"rgba(0,0,0,0.1)",lineWidth:1,borderDash:[],borderDashOffset:0},gridLines:{circular:!1},ticks:{showLabelBackdrop:!0,backdropColor:"rgba(255,255,255,0.75)",backdropPaddingY:2,backdropPaddingX:2,callback:sn.formatters.linear},pointLabels:{display:!0,fontSize:10,callback:function(t){return t}}};function Bn(t){var e=t.ticks;return e.display&&t.display?zn(e.fontSize,Y.global.defaultFontSize)+2*e.backdropPaddingY:0}function jn(t,e,n,i,a){return t===i||t===a?{start:e-n/2,end:e+n/2}:t<i||t>a?{start:e-n,end:e}:{start:e,end:e+n}}function Un(t){return 0===t||180===t?"center":t<180?"left":"right"}function Gn(t,e,n,i){var a,r,o=n.y+i/2;if(B.isArray(e))for(a=0,r=e.length;a<r;++a)t.fillText(e[a],n.x,o),o+=i;else t.fillText(e,n.x,o)}function qn(t,e,n){90===t||270===t?n.y-=e.h/2:(t>270||t<90)&&(n.y-=e.h)}function Zn(t){return B.isNumber(t)?t:0}var $n=Cn.extend({setDimensions:function(){var t=this;t.width=t.maxWidth,t.height=t.maxHeight,t.paddingTop=Bn(t.options)/2,t.xCenter=Math.floor(t.width/2),t.yCenter=Math.floor((t.height-t.paddingTop)/2),t.drawingArea=Math.min(t.height-t.paddingTop,t.width)/2},determineDataLimits:function(){var t=this,e=t.chart,n=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;B.each(e.data.datasets,(function(a,r){if(e.isDatasetVisible(r)){var o=e.getDatasetMeta(r);B.each(a.data,(function(e,a){var r=+t.getRightValue(e);isNaN(r)||o.data[a].hidden||(n=Math.min(r,n),i=Math.max(r,i))}))}})),t.min=n===Number.POSITIVE_INFINITY?0:n,t.max=i===Number.NEGATIVE_INFINITY?0:i,t.handleTickRangeOptions()},_computeTickLimit:function(){return Math.ceil(this.drawingArea/Bn(this.options))},convertTicksToLabels:function(){var t=this;Cn.prototype.convertTicksToLabels.call(t),t.pointLabels=t.chart.data.labels.map((function(){var e=B.callback(t.options.pointLabels.callback,arguments,t);return e||0===e?e:""}))},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var t=this.options;t.display&&t.pointLabels.display?function(t){var e,n,i,a=B.options._parseFont(t.options.pointLabels),r={l:0,r:t.width,t:0,b:t.height-t.paddingTop},o={};t.ctx.font=a.string,t._pointLabelSizes=[];var s,l,u,d=t.chart.data.labels.length;for(e=0;e<d;e++){i=t.getPointPosition(e,t.drawingArea+5),s=t.ctx,l=a.lineHeight,u=t.pointLabels[e],n=B.isArray(u)?{w:B.longestText(s,s.font,u),h:u.length*l}:{w:s.measureText(u).width,h:l},t._pointLabelSizes[e]=n;var h=t.getIndexAngle(e),c=B.toDegrees(h)%360,f=jn(c,i.x,n.w,0,180),g=jn(c,i.y,n.h,90,270);f.start<r.l&&(r.l=f.start,o.l=h),f.end>r.r&&(r.r=f.end,o.r=h),g.start<r.t&&(r.t=g.start,o.t=h),g.end>r.b&&(r.b=g.end,o.b=h)}t.setReductions(t.drawingArea,r,o)}(this):this.setCenterPoint(0,0,0,0)},setReductions:function(t,e,n){var i=this,a=e.l/Math.sin(n.l),r=Math.max(e.r-i.width,0)/Math.sin(n.r),o=-e.t/Math.cos(n.t),s=-Math.max(e.b-(i.height-i.paddingTop),0)/Math.cos(n.b);a=Zn(a),r=Zn(r),o=Zn(o),s=Zn(s),i.drawingArea=Math.min(Math.floor(t-(a+r)/2),Math.floor(t-(o+s)/2)),i.setCenterPoint(a,r,o,s)},setCenterPoint:function(t,e,n,i){var a=this,r=a.width-e-a.drawingArea,o=t+a.drawingArea,s=n+a.drawingArea,l=a.height-a.paddingTop-i-a.drawingArea;a.xCenter=Math.floor((o+r)/2+a.left),a.yCenter=Math.floor((s+l)/2+a.top+a.paddingTop)},getIndexAngle:function(t){var e=this.chart,n=(t*(360/e.data.labels.length)+((e.options||{}).startAngle||0))%360;return(n<0?n+360:n)*Math.PI*2/360},getDistanceFromCenterForValue:function(t){var e=this;if(B.isNullOrUndef(t))return NaN;var n=e.drawingArea/(e.max-e.min);return e.options.ticks.reverse?(e.max-t)*n:(t-e.min)*n},getPointPosition:function(t,e){var n=this.getIndexAngle(t)-Math.PI/2;return{x:Math.cos(n)*e+this.xCenter,y:Math.sin(n)*e+this.yCenter}},getPointPositionForValue:function(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))},getBasePosition:function(t){var e=this.min,n=this.max;return this.getPointPositionForValue(t||0,this.beginAtZero?0:e<0&&n<0?n:e>0&&n>0?e:0)},_drawGrid:function(){var t,e,n,i=this,a=i.ctx,r=i.options,o=r.gridLines,s=r.angleLines,l=zn(s.lineWidth,o.lineWidth),u=zn(s.color,o.color);if(r.pointLabels.display&&function(t){var e=t.ctx,n=t.options,i=n.pointLabels,a=Bn(n),r=t.getDistanceFromCenterForValue(n.ticks.reverse?t.min:t.max),o=B.options._parseFont(i);e.save(),e.font=o.string,e.textBaseline="middle";for(var s=t.chart.data.labels.length-1;s>=0;s--){var l=0===s?a/2:0,u=t.getPointPosition(s,r+l+5),d=En(i.fontColor,s,Y.global.defaultFontColor);e.fillStyle=d;var h=t.getIndexAngle(s),c=B.toDegrees(h);e.textAlign=Un(c),qn(c,t._pointLabelSizes[s],u),Gn(e,t.pointLabels[s],u,o.lineHeight)}e.restore()}(i),o.display&&B.each(i.ticks,(function(t,n){0!==n&&(e=i.getDistanceFromCenterForValue(i.ticksAsNumbers[n]),function(t,e,n,i){var a,r=t.ctx,o=e.circular,s=t.chart.data.labels.length,l=En(e.color,i-1),u=En(e.lineWidth,i-1);if((o||s)&&l&&u){if(r.save(),r.strokeStyle=l,r.lineWidth=u,r.setLineDash&&(r.setLineDash(e.borderDash||[]),r.lineDashOffset=e.borderDashOffset||0),r.beginPath(),o)r.arc(t.xCenter,t.yCenter,n,0,2*Math.PI);else{a=t.getPointPosition(0,n),r.moveTo(a.x,a.y);for(var d=1;d<s;d++)a=t.getPointPosition(d,n),r.lineTo(a.x,a.y)}r.closePath(),r.stroke(),r.restore()}}(i,o,e,n))})),s.display&&l&&u){for(a.save(),a.lineWidth=l,a.strokeStyle=u,a.setLineDash&&(a.setLineDash(Vn([s.borderDash,o.borderDash,[]])),a.lineDashOffset=Vn([s.borderDashOffset,o.borderDashOffset,0])),t=i.chart.data.labels.length-1;t>=0;t--)e=i.getDistanceFromCenterForValue(r.ticks.reverse?i.min:i.max),n=i.getPointPosition(t,e),a.beginPath(),a.moveTo(i.xCenter,i.yCenter),a.lineTo(n.x,n.y),a.stroke();a.restore()}},_drawLabels:function(){var t=this,e=t.ctx,n=t.options.ticks;if(n.display){var i,a,r=t.getIndexAngle(0),o=B.options._parseFont(n),s=zn(n.fontColor,Y.global.defaultFontColor);e.save(),e.font=o.string,e.translate(t.xCenter,t.yCenter),e.rotate(r),e.textAlign="center",e.textBaseline="middle",B.each(t.ticks,(function(r,l){(0!==l||n.reverse)&&(i=t.getDistanceFromCenterForValue(t.ticksAsNumbers[l]),n.showLabelBackdrop&&(a=e.measureText(r).width,e.fillStyle=n.backdropColor,e.fillRect(-a/2-n.backdropPaddingX,-i-o.size/2-n.backdropPaddingY,a+2*n.backdropPaddingX,o.size+2*n.backdropPaddingY)),e.fillStyle=s,e.fillText(r,0,-i))})),e.restore()}},_drawTitle:B.noop}),Xn=Hn;$n._defaults=Xn;var Kn=B._deprecated,Jn=B.options.resolve,Qn=B.valueOrDefault,ti=Number.MIN_SAFE_INTEGER||-9007199254740991,ei=Number.MAX_SAFE_INTEGER||9007199254740991,ni={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},ii=Object.keys(ni);function ai(t,e){return t-e}function ri(t){return B.valueOrDefault(t.time.min,t.ticks.min)}function oi(t){return B.valueOrDefault(t.time.max,t.ticks.max)}function si(t,e,n,i){var a=function(t,e,n){for(var i,a,r,o=0,s=t.length-1;o>=0&&o<=s;){if(a=t[(i=o+s>>1)-1]||null,r=t[i],!a)return{lo:null,hi:r};if(r[e]<n)o=i+1;else{if(!(a[e]>n))return{lo:a,hi:r};s=i-1}}return{lo:r,hi:null}}(t,e,n),r=a.lo?a.hi?a.lo:t[t.length-2]:t[0],o=a.lo?a.hi?a.hi:t[t.length-1]:t[1],s=o[e]-r[e],l=s?(n-r[e])/s:0,u=(o[i]-r[i])*l;return r[i]+u}function li(t,e){var n=t._adapter,i=t.options.time,a=i.parser,r=a||i.format,o=e;return"function"==typeof a&&(o=a(o)),B.isFinite(o)||(o="string"==typeof r?n.parse(o,r):n.parse(o)),null!==o?+o:(a||"function"!=typeof r||(o=r(e),B.isFinite(o)||(o=n.parse(o))),o)}function ui(t,e){if(B.isNullOrUndef(e))return null;var n=t.options.time,i=li(t,t.getRightValue(e));return null===i?i:(n.round&&(i=+t._adapter.startOf(i,n.round)),i)}function di(t,e,n,i){var a,r,o,s=ii.length;for(a=ii.indexOf(t);a<s-1;++a)if(o=(r=ni[ii[a]]).steps?r.steps:ei,r.common&&Math.ceil((n-e)/(o*r.size))<=i)return ii[a];return ii[s-1]}function hi(t,e,n){var i,a,r=[],o={},s=e.length;for(i=0;i<s;++i)o[a=e[i]]=i,r.push({value:a,major:!1});return 0!==s&&n?function(t,e,n,i){var a,r,o=t._adapter,s=+o.startOf(e[0].value,i),l=e[e.length-1].value;for(a=s;a<=l;a=+o.add(a,1,i))(r=n[a])>=0&&(e[r].major=!0);return e}(t,r,o,n):r}var ci=_n.extend({initialize:function(){this.mergeTicksOptions(),_n.prototype.initialize.call(this)},update:function(){var t=this,e=t.options,n=e.time||(e.time={}),i=t._adapter=new on._date(e.adapters.date);return Kn("time scale",n.format,"time.format","time.parser"),Kn("time scale",n.min,"time.min","ticks.min"),Kn("time scale",n.max,"time.max","ticks.max"),B.mergeIf(n.displayFormats,i.formats()),_n.prototype.update.apply(t,arguments)},getRightValue:function(t){return t&&void 0!==t.t&&(t=t.t),_n.prototype.getRightValue.call(this,t)},determineDataLimits:function(){var t,e,n,i,a,r,o,s=this,l=s.chart,u=s._adapter,d=s.options,h=d.time.unit||"day",c=ei,f=ti,g=[],m=[],p=[],v=s._getLabels();for(t=0,n=v.length;t<n;++t)p.push(ui(s,v[t]));for(t=0,n=(l.data.datasets||[]).length;t<n;++t)if(l.isDatasetVisible(t))if(a=l.data.datasets[t].data,B.isObject(a[0]))for(m[t]=[],e=0,i=a.length;e<i;++e)r=ui(s,a[e]),g.push(r),m[t][e]=r;else m[t]=p.slice(0),o||(g=g.concat(p),o=!0);else m[t]=[];p.length&&(c=Math.min(c,p[0]),f=Math.max(f,p[p.length-1])),g.length&&(g=n>1?function(t){var e,n,i,a={},r=[];for(e=0,n=t.length;e<n;++e)a[i=t[e]]||(a[i]=!0,r.push(i));return r}(g).sort(ai):g.sort(ai),c=Math.min(c,g[0]),f=Math.max(f,g[g.length-1])),c=ui(s,ri(d))||c,f=ui(s,oi(d))||f,c=c===ei?+u.startOf(Date.now(),h):c,f=f===ti?+u.endOf(Date.now(),h)+1:f,s.min=Math.min(c,f),s.max=Math.max(c+1,f),s._table=[],s._timestamps={data:g,datasets:m,labels:p}},buildTicks:function(){var t,e,n,i=this,a=i.min,r=i.max,o=i.options,s=o.ticks,l=o.time,u=i._timestamps,d=[],h=i.getLabelCapacity(a),c=s.source,f=o.distribution;for(u="data"===c||"auto"===c&&"series"===f?u.data:"labels"===c?u.labels:function(t,e,n,i){var a,r=t._adapter,o=t.options,s=o.time,l=s.unit||di(s.minUnit,e,n,i),u=Jn([s.stepSize,s.unitStepSize,1]),d="week"===l&&s.isoWeekday,h=e,c=[];if(d&&(h=+r.startOf(h,"isoWeek",d)),h=+r.startOf(h,d?"day":l),r.diff(n,e,l)>1e5*u)throw e+" and "+n+" are too far apart with stepSize of "+u+" "+l;for(a=h;a<n;a=+r.add(a,u,l))c.push(a);return a!==n&&"ticks"!==o.bounds||c.push(a),c}(i,a,r,h),"ticks"===o.bounds&&u.length&&(a=u[0],r=u[u.length-1]),a=ui(i,ri(o))||a,r=ui(i,oi(o))||r,t=0,e=u.length;t<e;++t)(n=u[t])>=a&&n<=r&&d.push(n);return i.min=a,i.max=r,i._unit=l.unit||(s.autoSkip?di(l.minUnit,i.min,i.max,h):function(t,e,n,i,a){var r,o;for(r=ii.length-1;r>=ii.indexOf(n);r--)if(o=ii[r],ni[o].common&&t._adapter.diff(a,i,o)>=e-1)return o;return ii[n?ii.indexOf(n):0]}(i,d.length,l.minUnit,i.min,i.max)),i._majorUnit=s.major.enabled&&"year"!==i._unit?function(t){for(var e=ii.indexOf(t)+1,n=ii.length;e<n;++e)if(ni[ii[e]].common)return ii[e]}(i._unit):void 0,i._table=function(t,e,n,i){if("linear"===i||!t.length)return[{time:e,pos:0},{time:n,pos:1}];var a,r,o,s,l,u=[],d=[e];for(a=0,r=t.length;a<r;++a)(s=t[a])>e&&s<n&&d.push(s);for(d.push(n),a=0,r=d.length;a<r;++a)l=d[a+1],o=d[a-1],s=d[a],void 0!==o&&void 0!==l&&Math.round((l+o)/2)===s||u.push({time:s,pos:a/(r-1)});return u}(i._timestamps.data,a,r,f),i._offsets=function(t,e,n,i,a){var r,o,s=0,l=0;return a.offset&&e.length&&(r=si(t,"time",e[0],"pos"),s=1===e.length?1-r:(si(t,"time",e[1],"pos")-r)/2,o=si(t,"time",e[e.length-1],"pos"),l=1===e.length?o:(o-si(t,"time",e[e.length-2],"pos"))/2),{start:s,end:l,factor:1/(s+1+l)}}(i._table,d,0,0,o),s.reverse&&d.reverse(),hi(i,d,i._majorUnit)},getLabelForIndex:function(t,e){var n=this,i=n._adapter,a=n.chart.data,r=n.options.time,o=a.labels&&t<a.labels.length?a.labels[t]:"",s=a.datasets[e].data[t];return B.isObject(s)&&(o=n.getRightValue(s)),r.tooltipFormat?i.format(li(n,o),r.tooltipFormat):"string"==typeof o?o:i.format(li(n,o),r.displayFormats.datetime)},tickFormatFunction:function(t,e,n,i){var a=this._adapter,r=this.options,o=r.time.displayFormats,s=o[this._unit],l=this._majorUnit,u=o[l],d=n[e],h=r.ticks,c=l&&u&&d&&d.major,f=a.format(t,i||(c?u:s)),g=c?h.major:h.minor,m=Jn([g.callback,g.userCallback,h.callback,h.userCallback]);return m?m(f,e,n):f},convertTicksToLabels:function(t){var e,n,i=[];for(e=0,n=t.length;e<n;++e)i.push(this.tickFormatFunction(t[e].value,e,t));return i},getPixelForOffset:function(t){var e=this._offsets,n=si(this._table,"time",t,"pos");return this.getPixelForDecimal((e.start+n)*e.factor)},getPixelForValue:function(t,e,n){var i=null;if(void 0!==e&&void 0!==n&&(i=this._timestamps.datasets[n][e]),null===i&&(i=ui(this,t)),null!==i)return this.getPixelForOffset(i)},getPixelForTick:function(t){var e=this.getTicks();return t>=0&&t<e.length?this.getPixelForOffset(e[t].value):null},getValueForPixel:function(t){var e=this._offsets,n=this.getDecimalForPixel(t)/e.factor-e.end,i=si(this._table,"pos",n,"time");return this._adapter._create(i)},_getLabelSize:function(t){var e=this.options.ticks,n=this.ctx.measureText(t).width,i=B.toRadians(this.isHorizontal()?e.maxRotation:e.minRotation),a=Math.cos(i),r=Math.sin(i),o=Qn(e.fontSize,Y.global.defaultFontSize);return{w:n*a+o*r,h:n*r+o*a}},getLabelWidth:function(t){return this._getLabelSize(t).w},getLabelCapacity:function(t){var e=this,n=e.options.time,i=n.displayFormats,a=i[n.unit]||i.millisecond,r=e.tickFormatFunction(t,0,hi(e,[t],e._majorUnit),a),o=e._getLabelSize(r),s=Math.floor(e.isHorizontal()?e.width/o.w:e.height/o.h);return e.options.offset&&s--,s>0?s:1}}),fi={position:"bottom",distribution:"linear",bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{autoSkip:!1,source:"auto",major:{enabled:!1}}};ci._defaults=fi;var gi={category:kn,linear:An,logarithmic:Wn,radialLinear:$n,time:ci},mi=e((function(e,n){e.exports=function(){var n,i;function a(){return n.apply(null,arguments)}function r(t){return t instanceof Array||"[object Array]"===Object.prototype.toString.call(t)}function o(t){return null!=t&&"[object Object]"===Object.prototype.toString.call(t)}function s(t){return void 0===t}function l(t){return"number"==typeof t||"[object Number]"===Object.prototype.toString.call(t)}function u(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function d(t,e){var n,i=[];for(n=0;n<t.length;++n)i.push(e(t[n],n));return i}function h(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function c(t,e){for(var n in e)h(e,n)&&(t[n]=e[n]);return h(e,"toString")&&(t.toString=e.toString),h(e,"valueOf")&&(t.valueOf=e.valueOf),t}function f(t,e,n,i){return Ie(t,e,n,i,!0).utc()}function g(t){return null==t._pf&&(t._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null,rfc2822:!1,weekdayMismatch:!1}),t._pf}function m(t){if(null==t._isValid){var e=g(t),n=i.call(e.parsedDateParts,(function(t){return null!=t})),a=!isNaN(t._d.getTime())&&e.overflow<0&&!e.empty&&!e.invalidMonth&&!e.invalidWeekday&&!e.weekdayMismatch&&!e.nullInput&&!e.invalidFormat&&!e.userInvalidated&&(!e.meridiem||e.meridiem&&n);if(t._strict&&(a=a&&0===e.charsLeftOver&&0===e.unusedTokens.length&&void 0===e.bigHour),null!=Object.isFrozen&&Object.isFrozen(t))return a;t._isValid=a}return t._isValid}function p(t){var e=f(NaN);return null!=t?c(g(e),t):g(e).userInvalidated=!0,e}i=Array.prototype.some?Array.prototype.some:function(t){for(var e=Object(this),n=e.length>>>0,i=0;i<n;i++)if(i in e&&t.call(this,e[i],i,e))return!0;return!1};var v=a.momentProperties=[];function b(t,e){var n,i,a;if(s(e._isAMomentObject)||(t._isAMomentObject=e._isAMomentObject),s(e._i)||(t._i=e._i),s(e._f)||(t._f=e._f),s(e._l)||(t._l=e._l),s(e._strict)||(t._strict=e._strict),s(e._tzm)||(t._tzm=e._tzm),s(e._isUTC)||(t._isUTC=e._isUTC),s(e._offset)||(t._offset=e._offset),s(e._pf)||(t._pf=g(e)),s(e._locale)||(t._locale=e._locale),v.length>0)for(n=0;n<v.length;n++)s(a=e[i=v[n]])||(t[i]=a);return t}var y=!1;function x(t){b(this,t),this._d=new Date(null!=t._d?t._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===y&&(y=!0,a.updateOffset(this),y=!1)}function _(t){return t instanceof x||null!=t&&null!=t._isAMomentObject}function w(t){return t<0?Math.ceil(t)||0:Math.floor(t)}function k(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=w(e)),n}function M(t,e,n){var i,a=Math.min(t.length,e.length),r=Math.abs(t.length-e.length),o=0;for(i=0;i<a;i++)(n&&t[i]!==e[i]||!n&&k(t[i])!==k(e[i]))&&o++;return o+r}function S(t){!1===a.suppressDeprecationWarnings&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}function D(t,e){var n=!0;return c((function(){if(null!=a.deprecationHandler&&a.deprecationHandler(null,t),n){for(var i,r=[],o=0;o<arguments.length;o++){if(i="","object"==typeof arguments[o]){for(var s in i+="\n["+o+"] ",arguments[0])i+=s+": "+arguments[0][s]+", ";i=i.slice(0,-2)}else i=arguments[o];r.push(i)}S(t+"\nArguments: "+Array.prototype.slice.call(r).join("")+"\n"+(new Error).stack),n=!1}return e.apply(this,arguments)}),e)}var C,P={};function T(t,e){null!=a.deprecationHandler&&a.deprecationHandler(t,e),P[t]||(S(e),P[t]=!0)}function O(t){return t instanceof Function||"[object Function]"===Object.prototype.toString.call(t)}function A(t,e){var n,i=c({},t);for(n in e)h(e,n)&&(o(t[n])&&o(e[n])?(i[n]={},c(i[n],t[n]),c(i[n],e[n])):null!=e[n]?i[n]=e[n]:delete i[n]);for(n in t)h(t,n)&&!h(e,n)&&o(t[n])&&(i[n]=c({},i[n]));return i}function F(t){null!=t&&this.set(t)}a.suppressDeprecationWarnings=!1,a.deprecationHandler=null,C=Object.keys?Object.keys:function(t){var e,n=[];for(e in t)h(t,e)&&n.push(e);return n};var I={};function L(t,e){var n=t.toLowerCase();I[n]=I[n+"s"]=I[e]=t}function R(t){return"string"==typeof t?I[t]||I[t.toLowerCase()]:void 0}function N(t){var e,n,i={};for(n in t)h(t,n)&&(e=R(n))&&(i[e]=t[n]);return i}var W={};function Y(t,e){W[t]=e}function z(t,e,n){var i=""+Math.abs(t),a=e-i.length;return(t>=0?n?"+":"":"-")+Math.pow(10,Math.max(0,a)).toString().substr(1)+i}var E=/(\[[^\[]*\])|(\\)?([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,V=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,H={},B={};function j(t,e,n,i){var a=i;"string"==typeof i&&(a=function(){return this[i]()}),t&&(B[t]=a),e&&(B[e[0]]=function(){return z(a.apply(this,arguments),e[1],e[2])}),n&&(B[n]=function(){return this.localeData().ordinal(a.apply(this,arguments),t)})}function U(t,e){return t.isValid()?(e=G(e,t.localeData()),H[e]=H[e]||function(t){var e,n,i,a=t.match(E);for(e=0,n=a.length;e<n;e++)B[a[e]]?a[e]=B[a[e]]:a[e]=(i=a[e]).match(/\[[\s\S]/)?i.replace(/^\[|\]$/g,""):i.replace(/\\/g,"");return function(e){var i,r="";for(i=0;i<n;i++)r+=O(a[i])?a[i].call(e,t):a[i];return r}}(e),H[e](t)):t.localeData().invalidDate()}function G(t,e){var n=5;function i(t){return e.longDateFormat(t)||t}for(V.lastIndex=0;n>=0&&V.test(t);)t=t.replace(V,i),V.lastIndex=0,n-=1;return t}var q=/\d/,Z=/\d\d/,$=/\d{3}/,X=/\d{4}/,K=/[+-]?\d{6}/,J=/\d\d?/,Q=/\d\d\d\d?/,tt=/\d\d\d\d\d\d?/,et=/\d{1,3}/,nt=/\d{1,4}/,it=/[+-]?\d{1,6}/,at=/\d+/,rt=/[+-]?\d+/,ot=/Z|[+-]\d\d:?\d\d/gi,st=/Z|[+-]\d\d(?::?\d\d)?/gi,lt=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,ut={};function dt(t,e,n){ut[t]=O(e)?e:function(t,i){return t&&n?n:e}}function ht(t,e){return h(ut,t)?ut[t](e._strict,e._locale):new RegExp(ct(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,(function(t,e,n,i,a){return e||n||i||a}))))}function ct(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var ft={};function gt(t,e){var n,i=e;for("string"==typeof t&&(t=[t]),l(e)&&(i=function(t,n){n[e]=k(t)}),n=0;n<t.length;n++)ft[t[n]]=i}function mt(t,e){gt(t,(function(t,n,i,a){i._w=i._w||{},e(t,i._w,i,a)}))}function pt(t,e,n){null!=e&&h(ft,t)&&ft[t](e,n._a,n,t)}var vt=0,bt=1,yt=2,xt=3,_t=4,wt=5,kt=6,Mt=7,St=8;function Dt(t){return Ct(t)?366:365}function Ct(t){return t%4==0&&t%100!=0||t%400==0}j("Y",0,0,(function(){var t=this.year();return t<=9999?""+t:"+"+t})),j(0,["YY",2],0,(function(){return this.year()%100})),j(0,["YYYY",4],0,"year"),j(0,["YYYYY",5],0,"year"),j(0,["YYYYYY",6,!0],0,"year"),L("year","y"),Y("year",1),dt("Y",rt),dt("YY",J,Z),dt("YYYY",nt,X),dt("YYYYY",it,K),dt("YYYYYY",it,K),gt(["YYYYY","YYYYYY"],vt),gt("YYYY",(function(t,e){e[vt]=2===t.length?a.parseTwoDigitYear(t):k(t)})),gt("YY",(function(t,e){e[vt]=a.parseTwoDigitYear(t)})),gt("Y",(function(t,e){e[vt]=parseInt(t,10)})),a.parseTwoDigitYear=function(t){return k(t)+(k(t)>68?1900:2e3)};var Pt,Tt=Ot("FullYear",!0);function Ot(t,e){return function(n){return null!=n?(Ft(this,t,n),a.updateOffset(this,e),this):At(this,t)}}function At(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function Ft(t,e,n){t.isValid()&&!isNaN(n)&&("FullYear"===e&&Ct(t.year())&&1===t.month()&&29===t.date()?t._d["set"+(t._isUTC?"UTC":"")+e](n,t.month(),It(n,t.month())):t._d["set"+(t._isUTC?"UTC":"")+e](n))}function It(t,e){if(isNaN(t)||isNaN(e))return NaN;var n=function(t,e){return(t%e+e)%e}(e,12);return t+=(e-n)/12,1===n?Ct(t)?29:28:31-n%7%2}Pt=Array.prototype.indexOf?Array.prototype.indexOf:function(t){var e;for(e=0;e<this.length;++e)if(this[e]===t)return e;return-1},j("M",["MM",2],"Mo",(function(){return this.month()+1})),j("MMM",0,0,(function(t){return this.localeData().monthsShort(this,t)})),j("MMMM",0,0,(function(t){return this.localeData().months(this,t)})),L("month","M"),Y("month",8),dt("M",J),dt("MM",J,Z),dt("MMM",(function(t,e){return e.monthsShortRegex(t)})),dt("MMMM",(function(t,e){return e.monthsRegex(t)})),gt(["M","MM"],(function(t,e){e[bt]=k(t)-1})),gt(["MMM","MMMM"],(function(t,e,n,i){var a=n._locale.monthsParse(t,i,n._strict);null!=a?e[bt]=a:g(n).invalidMonth=t}));var Lt=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,Rt="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),Nt="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_");function Wt(t,e,n){var i,a,r,o=t.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],i=0;i<12;++i)r=f([2e3,i]),this._shortMonthsParse[i]=this.monthsShort(r,"").toLocaleLowerCase(),this._longMonthsParse[i]=this.months(r,"").toLocaleLowerCase();return n?"MMM"===e?-1!==(a=Pt.call(this._shortMonthsParse,o))?a:null:-1!==(a=Pt.call(this._longMonthsParse,o))?a:null:"MMM"===e?-1!==(a=Pt.call(this._shortMonthsParse,o))?a:-1!==(a=Pt.call(this._longMonthsParse,o))?a:null:-1!==(a=Pt.call(this._longMonthsParse,o))?a:-1!==(a=Pt.call(this._shortMonthsParse,o))?a:null}function Yt(t,e){var n;if(!t.isValid())return t;if("string"==typeof e)if(/^\d+$/.test(e))e=k(e);else if(!l(e=t.localeData().monthsParse(e)))return t;return n=Math.min(t.date(),It(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,n),t}function zt(t){return null!=t?(Yt(this,t),a.updateOffset(this,!0),this):At(this,"Month")}var Et=lt,Vt=lt;function Ht(){function t(t,e){return e.length-t.length}var e,n,i=[],a=[],r=[];for(e=0;e<12;e++)n=f([2e3,e]),i.push(this.monthsShort(n,"")),a.push(this.months(n,"")),r.push(this.months(n,"")),r.push(this.monthsShort(n,""));for(i.sort(t),a.sort(t),r.sort(t),e=0;e<12;e++)i[e]=ct(i[e]),a[e]=ct(a[e]);for(e=0;e<24;e++)r[e]=ct(r[e]);this._monthsRegex=new RegExp("^("+r.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+i.join("|")+")","i")}function Bt(t,e,n,i,a,r,o){var s;return t<100&&t>=0?(s=new Date(t+400,e,n,i,a,r,o),isFinite(s.getFullYear())&&s.setFullYear(t)):s=new Date(t,e,n,i,a,r,o),s}function jt(t){var e;if(t<100&&t>=0){var n=Array.prototype.slice.call(arguments);n[0]=t+400,e=new Date(Date.UTC.apply(null,n)),isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t)}else e=new Date(Date.UTC.apply(null,arguments));return e}function Ut(t,e,n){var i=7+e-n;return-(7+jt(t,0,i).getUTCDay()-e)%7+i-1}function Gt(t,e,n,i,a){var r,o,s=1+7*(e-1)+(7+n-i)%7+Ut(t,i,a);return s<=0?o=Dt(r=t-1)+s:s>Dt(t)?(r=t+1,o=s-Dt(t)):(r=t,o=s),{year:r,dayOfYear:o}}function qt(t,e,n){var i,a,r=Ut(t.year(),e,n),o=Math.floor((t.dayOfYear()-r-1)/7)+1;return o<1?i=o+Zt(a=t.year()-1,e,n):o>Zt(t.year(),e,n)?(i=o-Zt(t.year(),e,n),a=t.year()+1):(a=t.year(),i=o),{week:i,year:a}}function Zt(t,e,n){var i=Ut(t,e,n),a=Ut(t+1,e,n);return(Dt(t)-i+a)/7}function $t(t,e){return t.slice(e,7).concat(t.slice(0,e))}j("w",["ww",2],"wo","week"),j("W",["WW",2],"Wo","isoWeek"),L("week","w"),L("isoWeek","W"),Y("week",5),Y("isoWeek",5),dt("w",J),dt("ww",J,Z),dt("W",J),dt("WW",J,Z),mt(["w","ww","W","WW"],(function(t,e,n,i){e[i.substr(0,1)]=k(t)})),j("d",0,"do","day"),j("dd",0,0,(function(t){return this.localeData().weekdaysMin(this,t)})),j("ddd",0,0,(function(t){return this.localeData().weekdaysShort(this,t)})),j("dddd",0,0,(function(t){return this.localeData().weekdays(this,t)})),j("e",0,0,"weekday"),j("E",0,0,"isoWeekday"),L("day","d"),L("weekday","e"),L("isoWeekday","E"),Y("day",11),Y("weekday",11),Y("isoWeekday",11),dt("d",J),dt("e",J),dt("E",J),dt("dd",(function(t,e){return e.weekdaysMinRegex(t)})),dt("ddd",(function(t,e){return e.weekdaysShortRegex(t)})),dt("dddd",(function(t,e){return e.weekdaysRegex(t)})),mt(["dd","ddd","dddd"],(function(t,e,n,i){var a=n._locale.weekdaysParse(t,i,n._strict);null!=a?e.d=a:g(n).invalidWeekday=t})),mt(["d","e","E"],(function(t,e,n,i){e[i]=k(t)}));var Xt="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Kt="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Jt="Su_Mo_Tu_We_Th_Fr_Sa".split("_");function Qt(t,e,n){var i,a,r,o=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],i=0;i<7;++i)r=f([2e3,1]).day(i),this._minWeekdaysParse[i]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[i]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[i]=this.weekdays(r,"").toLocaleLowerCase();return n?"dddd"===e?-1!==(a=Pt.call(this._weekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:null:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:"dddd"===e?-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:null}var te=lt,ee=lt,ne=lt;function ie(){function t(t,e){return e.length-t.length}var e,n,i,a,r,o=[],s=[],l=[],u=[];for(e=0;e<7;e++)n=f([2e3,1]).day(e),i=this.weekdaysMin(n,""),a=this.weekdaysShort(n,""),r=this.weekdays(n,""),o.push(i),s.push(a),l.push(r),u.push(i),u.push(a),u.push(r);for(o.sort(t),s.sort(t),l.sort(t),u.sort(t),e=0;e<7;e++)s[e]=ct(s[e]),l[e]=ct(l[e]),u[e]=ct(u[e]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+o.join("|")+")","i")}function ae(){return this.hours()%12||12}function re(t,e){j(t,0,0,(function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)}))}function oe(t,e){return e._meridiemParse}j("H",["HH",2],0,"hour"),j("h",["hh",2],0,ae),j("k",["kk",2],0,(function(){return this.hours()||24})),j("hmm",0,0,(function(){return""+ae.apply(this)+z(this.minutes(),2)})),j("hmmss",0,0,(function(){return""+ae.apply(this)+z(this.minutes(),2)+z(this.seconds(),2)})),j("Hmm",0,0,(function(){return""+this.hours()+z(this.minutes(),2)})),j("Hmmss",0,0,(function(){return""+this.hours()+z(this.minutes(),2)+z(this.seconds(),2)})),re("a",!0),re("A",!1),L("hour","h"),Y("hour",13),dt("a",oe),dt("A",oe),dt("H",J),dt("h",J),dt("k",J),dt("HH",J,Z),dt("hh",J,Z),dt("kk",J,Z),dt("hmm",Q),dt("hmmss",tt),dt("Hmm",Q),dt("Hmmss",tt),gt(["H","HH"],xt),gt(["k","kk"],(function(t,e,n){var i=k(t);e[xt]=24===i?0:i})),gt(["a","A"],(function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t})),gt(["h","hh"],(function(t,e,n){e[xt]=k(t),g(n).bigHour=!0})),gt("hmm",(function(t,e,n){var i=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i)),g(n).bigHour=!0})),gt("hmmss",(function(t,e,n){var i=t.length-4,a=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i,2)),e[wt]=k(t.substr(a)),g(n).bigHour=!0})),gt("Hmm",(function(t,e,n){var i=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i))})),gt("Hmmss",(function(t,e,n){var i=t.length-4,a=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i,2)),e[wt]=k(t.substr(a))}));var se,le=Ot("Hours",!0),ue={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{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"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d 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"},months:Rt,monthsShort:Nt,week:{dow:0,doy:6},weekdays:Xt,weekdaysMin:Jt,weekdaysShort:Kt,meridiemParse:/[ap]\.?m?\.?/i},de={},he={};function ce(t){return t?t.toLowerCase().replace("_","-"):t}function fe(n){var i=null;if(!de[n]&&e&&e.exports)try{i=se._abbr,t(),ge(i)}catch(t){}return de[n]}function ge(t,e){var n;return t&&((n=s(e)?pe(t):me(t,e))?se=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+t+" not found. Did you forget to load it?")),se._abbr}function me(t,e){if(null!==e){var n,i=ue;if(e.abbr=t,null!=de[t])T("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."),i=de[t]._config;else if(null!=e.parentLocale)if(null!=de[e.parentLocale])i=de[e.parentLocale]._config;else{if(null==(n=fe(e.parentLocale)))return he[e.parentLocale]||(he[e.parentLocale]=[]),he[e.parentLocale].push({name:t,config:e}),null;i=n._config}return de[t]=new F(A(i,e)),he[t]&&he[t].forEach((function(t){me(t.name,t.config)})),ge(t),de[t]}return delete de[t],null}function pe(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return se;if(!r(t)){if(e=fe(t))return e;t=[t]}return function(t){for(var e,n,i,a,r=0;r<t.length;){for(e=(a=ce(t[r]).split("-")).length,n=(n=ce(t[r+1]))?n.split("-"):null;e>0;){if(i=fe(a.slice(0,e).join("-")))return i;if(n&&n.length>=e&&M(a,n,!0)>=e-1)break;e--}r++}return se}(t)}function ve(t){var e,n=t._a;return n&&-2===g(t).overflow&&(e=n[bt]<0||n[bt]>11?bt:n[yt]<1||n[yt]>It(n[vt],n[bt])?yt:n[xt]<0||n[xt]>24||24===n[xt]&&(0!==n[_t]||0!==n[wt]||0!==n[kt])?xt:n[_t]<0||n[_t]>59?_t:n[wt]<0||n[wt]>59?wt:n[kt]<0||n[kt]>999?kt:-1,g(t)._overflowDayOfYear&&(e<vt||e>yt)&&(e=yt),g(t)._overflowWeeks&&-1===e&&(e=Mt),g(t)._overflowWeekday&&-1===e&&(e=St),g(t).overflow=e),t}function be(t,e,n){return null!=t?t:null!=e?e:n}function ye(t){var e,n,i,r,o,s=[];if(!t._d){for(i=function(t){var e=new Date(a.now());return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}(t),t._w&&null==t._a[yt]&&null==t._a[bt]&&function(t){var e,n,i,a,r,o,s,l;if(null!=(e=t._w).GG||null!=e.W||null!=e.E)r=1,o=4,n=be(e.GG,t._a[vt],qt(Le(),1,4).year),i=be(e.W,1),((a=be(e.E,1))<1||a>7)&&(l=!0);else{r=t._locale._week.dow,o=t._locale._week.doy;var u=qt(Le(),r,o);n=be(e.gg,t._a[vt],u.year),i=be(e.w,u.week),null!=e.d?((a=e.d)<0||a>6)&&(l=!0):null!=e.e?(a=e.e+r,(e.e<0||e.e>6)&&(l=!0)):a=r}i<1||i>Zt(n,r,o)?g(t)._overflowWeeks=!0:null!=l?g(t)._overflowWeekday=!0:(s=Gt(n,i,a,r,o),t._a[vt]=s.year,t._dayOfYear=s.dayOfYear)}(t),null!=t._dayOfYear&&(o=be(t._a[vt],i[vt]),(t._dayOfYear>Dt(o)||0===t._dayOfYear)&&(g(t)._overflowDayOfYear=!0),n=jt(o,0,t._dayOfYear),t._a[bt]=n.getUTCMonth(),t._a[yt]=n.getUTCDate()),e=0;e<3&&null==t._a[e];++e)t._a[e]=s[e]=i[e];for(;e<7;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[xt]&&0===t._a[_t]&&0===t._a[wt]&&0===t._a[kt]&&(t._nextDay=!0,t._a[xt]=0),t._d=(t._useUTC?jt:Bt).apply(null,s),r=t._useUTC?t._d.getUTCDay():t._d.getDay(),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[xt]=24),t._w&&void 0!==t._w.d&&t._w.d!==r&&(g(t).weekdayMismatch=!0)}}var xe=/^\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)?)?$/,_e=/^\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)?)?$/,we=/Z|[+-]\d\d(?::?\d\d)?/,ke=[["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}/]],Me=[["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/]],Se=/^\/?Date\((\-?\d+)/i;function De(t){var e,n,i,a,r,o,s=t._i,l=xe.exec(s)||_e.exec(s);if(l){for(g(t).iso=!0,e=0,n=ke.length;e<n;e++)if(ke[e][1].exec(l[1])){a=ke[e][0],i=!1!==ke[e][2];break}if(null==a)return void(t._isValid=!1);if(l[3]){for(e=0,n=Me.length;e<n;e++)if(Me[e][1].exec(l[3])){r=(l[2]||" ")+Me[e][0];break}if(null==r)return void(t._isValid=!1)}if(!i&&null!=r)return void(t._isValid=!1);if(l[4]){if(!we.exec(l[4]))return void(t._isValid=!1);o="Z"}t._f=a+(r||"")+(o||""),Ae(t)}else t._isValid=!1}var Ce=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/;function Pe(t){var e=parseInt(t,10);return e<=49?2e3+e:e<=999?1900+e:e}var Te={UT:0,GMT:0,EDT:-240,EST:-300,CDT:-300,CST:-360,MDT:-360,MST:-420,PDT:-420,PST:-480};function Oe(t){var e,n,i,a,r,o,s,l=Ce.exec(t._i.replace(/\([^)]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").replace(/^\s\s*/,"").replace(/\s\s*$/,""));if(l){var u=(e=l[4],n=l[3],i=l[2],a=l[5],r=l[6],o=l[7],s=[Pe(e),Nt.indexOf(n),parseInt(i,10),parseInt(a,10),parseInt(r,10)],o&&s.push(parseInt(o,10)),s);if(!function(t,e,n){return!t||Kt.indexOf(t)===new Date(e[0],e[1],e[2]).getDay()||(g(n).weekdayMismatch=!0,n._isValid=!1,!1)}(l[1],u,t))return;t._a=u,t._tzm=function(t,e,n){if(t)return Te[t];if(e)return 0;var i=parseInt(n,10),a=i%100;return(i-a)/100*60+a}(l[8],l[9],l[10]),t._d=jt.apply(null,t._a),t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),g(t).rfc2822=!0}else t._isValid=!1}function Ae(t){if(t._f!==a.ISO_8601)if(t._f!==a.RFC_2822){t._a=[],g(t).empty=!0;var e,n,i,r,o,s=""+t._i,l=s.length,u=0;for(i=G(t._f,t._locale).match(E)||[],e=0;e<i.length;e++)r=i[e],(n=(s.match(ht(r,t))||[])[0])&&((o=s.substr(0,s.indexOf(n))).length>0&&g(t).unusedInput.push(o),s=s.slice(s.indexOf(n)+n.length),u+=n.length),B[r]?(n?g(t).empty=!1:g(t).unusedTokens.push(r),pt(r,n,t)):t._strict&&!n&&g(t).unusedTokens.push(r);g(t).charsLeftOver=l-u,s.length>0&&g(t).unusedInput.push(s),t._a[xt]<=12&&!0===g(t).bigHour&&t._a[xt]>0&&(g(t).bigHour=void 0),g(t).parsedDateParts=t._a.slice(0),g(t).meridiem=t._meridiem,t._a[xt]=function(t,e,n){var i;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?((i=t.isPM(n))&&e<12&&(e+=12),i||12!==e||(e=0),e):e}(t._locale,t._a[xt],t._meridiem),ye(t),ve(t)}else Oe(t);else De(t)}function Fe(t){var e=t._i,n=t._f;return t._locale=t._locale||pe(t._l),null===e||void 0===n&&""===e?p({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),_(e)?new x(ve(e)):(u(e)?t._d=e:r(n)?function(t){var e,n,i,a,r;if(0===t._f.length)return g(t).invalidFormat=!0,void(t._d=new Date(NaN));for(a=0;a<t._f.length;a++)r=0,e=b({},t),null!=t._useUTC&&(e._useUTC=t._useUTC),e._f=t._f[a],Ae(e),m(e)&&(r+=g(e).charsLeftOver,r+=10*g(e).unusedTokens.length,g(e).score=r,(null==i||r<i)&&(i=r,n=e));c(t,n||e)}(t):n?Ae(t):function(t){var e=t._i;s(e)?t._d=new Date(a.now()):u(e)?t._d=new Date(e.valueOf()):"string"==typeof e?function(t){var e=Se.exec(t._i);null===e?(De(t),!1===t._isValid&&(delete t._isValid,Oe(t),!1===t._isValid&&(delete t._isValid,a.createFromInputFallback(t)))):t._d=new Date(+e[1])}(t):r(e)?(t._a=d(e.slice(0),(function(t){return parseInt(t,10)})),ye(t)):o(e)?function(t){if(!t._d){var e=N(t._i);t._a=d([e.year,e.month,e.day||e.date,e.hour,e.minute,e.second,e.millisecond],(function(t){return t&&parseInt(t,10)})),ye(t)}}(t):l(e)?t._d=new Date(e):a.createFromInputFallback(t)}(t),m(t)||(t._d=null),t))}function Ie(t,e,n,i,a){var s,l={};return!0!==n&&!1!==n||(i=n,n=void 0),(o(t)&&function(t){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(t).length;var e;for(e in t)if(t.hasOwnProperty(e))return!1;return!0}(t)||r(t)&&0===t.length)&&(t=void 0),l._isAMomentObject=!0,l._useUTC=l._isUTC=a,l._l=n,l._i=t,l._f=e,l._strict=i,(s=new x(ve(Fe(l))))._nextDay&&(s.add(1,"d"),s._nextDay=void 0),s}function Le(t,e,n,i){return Ie(t,e,n,i,!1)}a.createFromInputFallback=D("value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",(function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))})),a.ISO_8601=function(){},a.RFC_2822=function(){};var Re=D("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",(function(){var t=Le.apply(null,arguments);return this.isValid()&&t.isValid()?t<this?this:t:p()})),Ne=D("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",(function(){var t=Le.apply(null,arguments);return this.isValid()&&t.isValid()?t>this?this:t:p()}));function We(t,e){var n,i;if(1===e.length&&r(e[0])&&(e=e[0]),!e.length)return Le();for(n=e[0],i=1;i<e.length;++i)e[i].isValid()&&!e[i][t](n)||(n=e[i]);return n}var Ye=["year","quarter","month","week","day","hour","minute","second","millisecond"];function ze(t){var e=N(t),n=e.year||0,i=e.quarter||0,a=e.month||0,r=e.week||e.isoWeek||0,o=e.day||0,s=e.hour||0,l=e.minute||0,u=e.second||0,d=e.millisecond||0;this._isValid=function(t){for(var e in t)if(-1===Pt.call(Ye,e)||null!=t[e]&&isNaN(t[e]))return!1;for(var n=!1,i=0;i<Ye.length;++i)if(t[Ye[i]]){if(n)return!1;parseFloat(t[Ye[i]])!==k(t[Ye[i]])&&(n=!0)}return!0}(e),this._milliseconds=+d+1e3*u+6e4*l+1e3*s*60*60,this._days=+o+7*r,this._months=+a+3*i+12*n,this._data={},this._locale=pe(),this._bubble()}function Ee(t){return t instanceof ze}function Ve(t){return t<0?-1*Math.round(-1*t):Math.round(t)}function He(t,e){j(t,0,0,(function(){var t=this.utcOffset(),n="+";return t<0&&(t=-t,n="-"),n+z(~~(t/60),2)+e+z(~~t%60,2)}))}He("Z",":"),He("ZZ",""),dt("Z",st),dt("ZZ",st),gt(["Z","ZZ"],(function(t,e,n){n._useUTC=!0,n._tzm=je(st,t)}));var Be=/([\+\-]|\d\d)/gi;function je(t,e){var n=(e||"").match(t);if(null===n)return null;var i=((n[n.length-1]||[])+"").match(Be)||["-",0,0],a=60*i[1]+k(i[2]);return 0===a?0:"+"===i[0]?a:-a}function Ue(t,e){var n,i;return e._isUTC?(n=e.clone(),i=(_(t)||u(t)?t.valueOf():Le(t).valueOf())-n.valueOf(),n._d.setTime(n._d.valueOf()+i),a.updateOffset(n,!1),n):Le(t).local()}function Ge(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function qe(){return!!this.isValid()&&this._isUTC&&0===this._offset}a.updateOffset=function(){};var Ze=/^(\-|\+)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,$e=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;function Xe(t,e){var n,i,a,r,o,s,u=t,d=null;return Ee(t)?u={ms:t._milliseconds,d:t._days,M:t._months}:l(t)?(u={},e?u[e]=t:u.milliseconds=t):(d=Ze.exec(t))?(n="-"===d[1]?-1:1,u={y:0,d:k(d[yt])*n,h:k(d[xt])*n,m:k(d[_t])*n,s:k(d[wt])*n,ms:k(Ve(1e3*d[kt]))*n}):(d=$e.exec(t))?(n="-"===d[1]?-1:1,u={y:Ke(d[2],n),M:Ke(d[3],n),w:Ke(d[4],n),d:Ke(d[5],n),h:Ke(d[6],n),m:Ke(d[7],n),s:Ke(d[8],n)}):null==u?u={}:"object"==typeof u&&("from"in u||"to"in u)&&(r=Le(u.from),o=Le(u.to),a=r.isValid()&&o.isValid()?(o=Ue(o,r),r.isBefore(o)?s=Je(r,o):((s=Je(o,r)).milliseconds=-s.milliseconds,s.months=-s.months),s):{milliseconds:0,months:0},(u={}).ms=a.milliseconds,u.M=a.months),i=new ze(u),Ee(t)&&h(t,"_locale")&&(i._locale=t._locale),i}function Ke(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Je(t,e){var n={};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 Qe(t,e){return function(n,i){var a;return null===i||isNaN(+i)||(T(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."),a=n,n=i,i=a),tn(this,Xe(n="string"==typeof n?+n:n,i),t),this}}function tn(t,e,n,i){var r=e._milliseconds,o=Ve(e._days),s=Ve(e._months);t.isValid()&&(i=null==i||i,s&&Yt(t,At(t,"Month")+s*n),o&&Ft(t,"Date",At(t,"Date")+o*n),r&&t._d.setTime(t._d.valueOf()+r*n),i&&a.updateOffset(t,o||s))}Xe.fn=ze.prototype,Xe.invalid=function(){return Xe(NaN)};var en=Qe(1,"add"),nn=Qe(-1,"subtract");function an(t,e){var n=12*(e.year()-t.year())+(e.month()-t.month()),i=t.clone().add(n,"months");return-(n+(e-i<0?(e-i)/(i-t.clone().add(n-1,"months")):(e-i)/(t.clone().add(n+1,"months")-i)))||0}function rn(t){var e;return void 0===t?this._locale._abbr:(null!=(e=pe(t))&&(this._locale=e),this)}a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var on=D("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)}));function sn(){return this._locale}var ln=1e3,un=60*ln,dn=60*un,hn=3506328*dn;function cn(t,e){return(t%e+e)%e}function fn(t,e,n){return t<100&&t>=0?new Date(t+400,e,n)-hn:new Date(t,e,n).valueOf()}function gn(t,e,n){return t<100&&t>=0?Date.UTC(t+400,e,n)-hn:Date.UTC(t,e,n)}function mn(t,e){j(0,[t,t.length],0,e)}function pn(t,e,n,i,a){var r;return null==t?qt(this,i,a).year:(e>(r=Zt(t,i,a))&&(e=r),vn.call(this,t,e,n,i,a))}function vn(t,e,n,i,a){var r=Gt(t,e,n,i,a),o=jt(r.year,0,r.dayOfYear);return this.year(o.getUTCFullYear()),this.month(o.getUTCMonth()),this.date(o.getUTCDate()),this}j(0,["gg",2],0,(function(){return this.weekYear()%100})),j(0,["GG",2],0,(function(){return this.isoWeekYear()%100})),mn("gggg","weekYear"),mn("ggggg","weekYear"),mn("GGGG","isoWeekYear"),mn("GGGGG","isoWeekYear"),L("weekYear","gg"),L("isoWeekYear","GG"),Y("weekYear",1),Y("isoWeekYear",1),dt("G",rt),dt("g",rt),dt("GG",J,Z),dt("gg",J,Z),dt("GGGG",nt,X),dt("gggg",nt,X),dt("GGGGG",it,K),dt("ggggg",it,K),mt(["gggg","ggggg","GGGG","GGGGG"],(function(t,e,n,i){e[i.substr(0,2)]=k(t)})),mt(["gg","GG"],(function(t,e,n,i){e[i]=a.parseTwoDigitYear(t)})),j("Q",0,"Qo","quarter"),L("quarter","Q"),Y("quarter",7),dt("Q",q),gt("Q",(function(t,e){e[bt]=3*(k(t)-1)})),j("D",["DD",2],"Do","date"),L("date","D"),Y("date",9),dt("D",J),dt("DD",J,Z),dt("Do",(function(t,e){return t?e._dayOfMonthOrdinalParse||e._ordinalParse:e._dayOfMonthOrdinalParseLenient})),gt(["D","DD"],yt),gt("Do",(function(t,e){e[yt]=k(t.match(J)[0])}));var bn=Ot("Date",!0);j("DDD",["DDDD",3],"DDDo","dayOfYear"),L("dayOfYear","DDD"),Y("dayOfYear",4),dt("DDD",et),dt("DDDD",$),gt(["DDD","DDDD"],(function(t,e,n){n._dayOfYear=k(t)})),j("m",["mm",2],0,"minute"),L("minute","m"),Y("minute",14),dt("m",J),dt("mm",J,Z),gt(["m","mm"],_t);var yn=Ot("Minutes",!1);j("s",["ss",2],0,"second"),L("second","s"),Y("second",15),dt("s",J),dt("ss",J,Z),gt(["s","ss"],wt);var xn,_n=Ot("Seconds",!1);for(j("S",0,0,(function(){return~~(this.millisecond()/100)})),j(0,["SS",2],0,(function(){return~~(this.millisecond()/10)})),j(0,["SSS",3],0,"millisecond"),j(0,["SSSS",4],0,(function(){return 10*this.millisecond()})),j(0,["SSSSS",5],0,(function(){return 100*this.millisecond()})),j(0,["SSSSSS",6],0,(function(){return 1e3*this.millisecond()})),j(0,["SSSSSSS",7],0,(function(){return 1e4*this.millisecond()})),j(0,["SSSSSSSS",8],0,(function(){return 1e5*this.millisecond()})),j(0,["SSSSSSSSS",9],0,(function(){return 1e6*this.millisecond()})),L("millisecond","ms"),Y("millisecond",16),dt("S",et,q),dt("SS",et,Z),dt("SSS",et,$),xn="SSSS";xn.length<=9;xn+="S")dt(xn,at);function wn(t,e){e[kt]=k(1e3*("0."+t))}for(xn="S";xn.length<=9;xn+="S")gt(xn,wn);var kn=Ot("Milliseconds",!1);j("z",0,0,"zoneAbbr"),j("zz",0,0,"zoneName");var Mn=x.prototype;function Sn(t){return t}Mn.add=en,Mn.calendar=function(t,e){var n=t||Le(),i=Ue(n,this).startOf("day"),r=a.calendarFormat(this,i)||"sameElse",o=e&&(O(e[r])?e[r].call(this,n):e[r]);return this.format(o||this.localeData().calendar(r,this,Le(n)))},Mn.clone=function(){return new x(this)},Mn.diff=function(t,e,n){var i,a,r;if(!this.isValid())return NaN;if(!(i=Ue(t,this)).isValid())return NaN;switch(a=6e4*(i.utcOffset()-this.utcOffset()),e=R(e)){case"year":r=an(this,i)/12;break;case"month":r=an(this,i);break;case"quarter":r=an(this,i)/3;break;case"second":r=(this-i)/1e3;break;case"minute":r=(this-i)/6e4;break;case"hour":r=(this-i)/36e5;break;case"day":r=(this-i-a)/864e5;break;case"week":r=(this-i-a)/6048e5;break;default:r=this-i}return n?r:w(r)},Mn.endOf=function(t){var e;if(void 0===(t=R(t))||"millisecond"===t||!this.isValid())return this;var n=this._isUTC?gn:fn;switch(t){case"year":e=n(this.year()+1,0,1)-1;break;case"quarter":e=n(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":e=n(this.year(),this.month()+1,1)-1;break;case"week":e=n(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":e=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":e=n(this.year(),this.month(),this.date()+1)-1;break;case"hour":e=this._d.valueOf(),e+=dn-cn(e+(this._isUTC?0:this.utcOffset()*un),dn)-1;break;case"minute":e=this._d.valueOf(),e+=un-cn(e,un)-1;break;case"second":e=this._d.valueOf(),e+=ln-cn(e,ln)-1}return this._d.setTime(e),a.updateOffset(this,!0),this},Mn.format=function(t){t||(t=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var e=U(this,t);return this.localeData().postformat(e)},Mn.from=function(t,e){return this.isValid()&&(_(t)&&t.isValid()||Le(t).isValid())?Xe({to:this,from:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},Mn.fromNow=function(t){return this.from(Le(),t)},Mn.to=function(t,e){return this.isValid()&&(_(t)&&t.isValid()||Le(t).isValid())?Xe({from:this,to:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},Mn.toNow=function(t){return this.to(Le(),t)},Mn.get=function(t){return O(this[t=R(t)])?this[t]():this},Mn.invalidAt=function(){return g(this).overflow},Mn.isAfter=function(t,e){var n=_(t)?t:Le(t);return!(!this.isValid()||!n.isValid())&&("millisecond"===(e=R(e)||"millisecond")?this.valueOf()>n.valueOf():n.valueOf()<this.clone().startOf(e).valueOf())},Mn.isBefore=function(t,e){var n=_(t)?t:Le(t);return!(!this.isValid()||!n.isValid())&&("millisecond"===(e=R(e)||"millisecond")?this.valueOf()<n.valueOf():this.clone().endOf(e).valueOf()<n.valueOf())},Mn.isBetween=function(t,e,n,i){var a=_(t)?t:Le(t),r=_(e)?e:Le(e);return!!(this.isValid()&&a.isValid()&&r.isValid())&&("("===(i=i||"()")[0]?this.isAfter(a,n):!this.isBefore(a,n))&&(")"===i[1]?this.isBefore(r,n):!this.isAfter(r,n))},Mn.isSame=function(t,e){var n,i=_(t)?t:Le(t);return!(!this.isValid()||!i.isValid())&&("millisecond"===(e=R(e)||"millisecond")?this.valueOf()===i.valueOf():(n=i.valueOf(),this.clone().startOf(e).valueOf()<=n&&n<=this.clone().endOf(e).valueOf()))},Mn.isSameOrAfter=function(t,e){return this.isSame(t,e)||this.isAfter(t,e)},Mn.isSameOrBefore=function(t,e){return this.isSame(t,e)||this.isBefore(t,e)},Mn.isValid=function(){return m(this)},Mn.lang=on,Mn.locale=rn,Mn.localeData=sn,Mn.max=Ne,Mn.min=Re,Mn.parsingFlags=function(){return c({},g(this))},Mn.set=function(t,e){if("object"==typeof t)for(var n=function(t){var e=[];for(var n in t)e.push({unit:n,priority:W[n]});return e.sort((function(t,e){return t.priority-e.priority})),e}(t=N(t)),i=0;i<n.length;i++)this[n[i].unit](t[n[i].unit]);else if(O(this[t=R(t)]))return this[t](e);return this},Mn.startOf=function(t){var e;if(void 0===(t=R(t))||"millisecond"===t||!this.isValid())return this;var n=this._isUTC?gn:fn;switch(t){case"year":e=n(this.year(),0,1);break;case"quarter":e=n(this.year(),this.month()-this.month()%3,1);break;case"month":e=n(this.year(),this.month(),1);break;case"week":e=n(this.year(),this.month(),this.date()-this.weekday());break;case"isoWeek":e=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1));break;case"day":case"date":e=n(this.year(),this.month(),this.date());break;case"hour":e=this._d.valueOf(),e-=cn(e+(this._isUTC?0:this.utcOffset()*un),dn);break;case"minute":e=this._d.valueOf(),e-=cn(e,un);break;case"second":e=this._d.valueOf(),e-=cn(e,ln)}return this._d.setTime(e),a.updateOffset(this,!0),this},Mn.subtract=nn,Mn.toArray=function(){var t=this;return[t.year(),t.month(),t.date(),t.hour(),t.minute(),t.second(),t.millisecond()]},Mn.toObject=function(){var t=this;return{years:t.year(),months:t.month(),date:t.date(),hours:t.hours(),minutes:t.minutes(),seconds:t.seconds(),milliseconds:t.milliseconds()}},Mn.toDate=function(){return new Date(this.valueOf())},Mn.toISOString=function(t){if(!this.isValid())return null;var e=!0!==t,n=e?this.clone().utc():this;return n.year()<0||n.year()>9999?U(n,e?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):O(Date.prototype.toISOString)?e?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",U(n,"Z")):U(n,e?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},Mn.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var t="moment",e="";this.isLocal()||(t=0===this.utcOffset()?"moment.utc":"moment.parseZone",e="Z");var n="["+t+'("]',i=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",a=e+'[")]';return this.format(n+i+"-MM-DD[T]HH:mm:ss.SSS"+a)},Mn.toJSON=function(){return this.isValid()?this.toISOString():null},Mn.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},Mn.unix=function(){return Math.floor(this.valueOf()/1e3)},Mn.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},Mn.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},Mn.year=Tt,Mn.isLeapYear=function(){return Ct(this.year())},Mn.weekYear=function(t){return pn.call(this,t,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},Mn.isoWeekYear=function(t){return pn.call(this,t,this.isoWeek(),this.isoWeekday(),1,4)},Mn.quarter=Mn.quarters=function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},Mn.month=zt,Mn.daysInMonth=function(){return It(this.year(),this.month())},Mn.week=Mn.weeks=function(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")},Mn.isoWeek=Mn.isoWeeks=function(t){var e=qt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")},Mn.weeksInYear=function(){var t=this.localeData()._week;return Zt(this.year(),t.dow,t.doy)},Mn.isoWeeksInYear=function(){return Zt(this.year(),1,4)},Mn.date=bn,Mn.day=Mn.days=function(t){if(!this.isValid())return null!=t?this:NaN;var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=function(t,e){return"string"!=typeof t?t:isNaN(t)?"number"==typeof(t=e.weekdaysParse(t))?t:null:parseInt(t,10)}(t,this.localeData()),this.add(t-e,"d")):e},Mn.weekday=function(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")},Mn.isoWeekday=function(t){if(!this.isValid())return null!=t?this:NaN;if(null!=t){var e=function(t,e){return"string"==typeof t?e.weekdaysParse(t)%7||7:isNaN(t)?null:t}(t,this.localeData());return this.day(this.day()%7?e:e-7)}return this.day()||7},Mn.dayOfYear=function(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")},Mn.hour=Mn.hours=le,Mn.minute=Mn.minutes=yn,Mn.second=Mn.seconds=_n,Mn.millisecond=Mn.milliseconds=kn,Mn.utcOffset=function(t,e,n){var i,r=this._offset||0;if(!this.isValid())return null!=t?this:NaN;if(null!=t){if("string"==typeof t){if(null===(t=je(st,t)))return this}else Math.abs(t)<16&&!n&&(t*=60);return!this._isUTC&&e&&(i=Ge(this)),this._offset=t,this._isUTC=!0,null!=i&&this.add(i,"m"),r!==t&&(!e||this._changeInProgress?tn(this,Xe(t-r,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?r:Ge(this)},Mn.utc=function(t){return this.utcOffset(0,t)},Mn.local=function(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Ge(this),"m")),this},Mn.parseZone=function(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var t=je(ot,this._i);null!=t?this.utcOffset(t):this.utcOffset(0,!0)}return this},Mn.hasAlignedHourOffset=function(t){return!!this.isValid()&&(t=t?Le(t).utcOffset():0,(this.utcOffset()-t)%60==0)},Mn.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},Mn.isLocal=function(){return!!this.isValid()&&!this._isUTC},Mn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},Mn.isUtc=qe,Mn.isUTC=qe,Mn.zoneAbbr=function(){return this._isUTC?"UTC":""},Mn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},Mn.dates=D("dates accessor is deprecated. Use date instead.",bn),Mn.months=D("months accessor is deprecated. Use month instead",zt),Mn.years=D("years accessor is deprecated. Use year instead",Tt),Mn.zone=D("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",(function(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()})),Mn.isDSTShifted=D("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",(function(){if(!s(this._isDSTShifted))return this._isDSTShifted;var t={};if(b(t,this),(t=Fe(t))._a){var e=t._isUTC?f(t._a):Le(t._a);this._isDSTShifted=this.isValid()&&M(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}));var Dn=F.prototype;function Cn(t,e,n,i){var a=pe(),r=f().set(i,e);return a[n](r,t)}function Pn(t,e,n){if(l(t)&&(e=t,t=void 0),t=t||"",null!=e)return Cn(t,e,n,"month");var i,a=[];for(i=0;i<12;i++)a[i]=Cn(t,i,n,"month");return a}function Tn(t,e,n,i){"boolean"==typeof t?(l(e)&&(n=e,e=void 0),e=e||""):(n=e=t,t=!1,l(e)&&(n=e,e=void 0),e=e||"");var a,r=pe(),o=t?r._week.dow:0;if(null!=n)return Cn(e,(n+o)%7,i,"day");var s=[];for(a=0;a<7;a++)s[a]=Cn(e,(a+o)%7,i,"day");return s}Dn.calendar=function(t,e,n){var i=this._calendar[t]||this._calendar.sameElse;return O(i)?i.call(e,n):i},Dn.longDateFormat=function(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])},Dn.invalidDate=function(){return this._invalidDate},Dn.ordinal=function(t){return this._ordinal.replace("%d",t)},Dn.preparse=Sn,Dn.postformat=Sn,Dn.relativeTime=function(t,e,n,i){var a=this._relativeTime[n];return O(a)?a(t,e,n,i):a.replace(/%d/i,t)},Dn.pastFuture=function(t,e){var n=this._relativeTime[t>0?"future":"past"];return O(n)?n(e):n.replace(/%s/i,e)},Dn.set=function(t){var e,n;for(n in t)O(e=t[n])?this[n]=e:this["_"+n]=e;this._config=t,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},Dn.months=function(t,e){return t?r(this._months)?this._months[t.month()]:this._months[(this._months.isFormat||Lt).test(e)?"format":"standalone"][t.month()]:r(this._months)?this._months:this._months.standalone},Dn.monthsShort=function(t,e){return t?r(this._monthsShort)?this._monthsShort[t.month()]:this._monthsShort[Lt.test(e)?"format":"standalone"][t.month()]:r(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},Dn.monthsParse=function(t,e,n){var i,a,r;if(this._monthsParseExact)return Wt.call(this,t,e,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),i=0;i<12;i++){if(a=f([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]||(r="^"+this.months(a,"")+"|^"+this.monthsShort(a,""),this._monthsParse[i]=new RegExp(r.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}},Dn.monthsRegex=function(t){return this._monthsParseExact?(h(this,"_monthsRegex")||Ht.call(this),t?this._monthsStrictRegex:this._monthsRegex):(h(this,"_monthsRegex")||(this._monthsRegex=Vt),this._monthsStrictRegex&&t?this._monthsStrictRegex:this._monthsRegex)},Dn.monthsShortRegex=function(t){return this._monthsParseExact?(h(this,"_monthsRegex")||Ht.call(this),t?this._monthsShortStrictRegex:this._monthsShortRegex):(h(this,"_monthsShortRegex")||(this._monthsShortRegex=Et),this._monthsShortStrictRegex&&t?this._monthsShortStrictRegex:this._monthsShortRegex)},Dn.week=function(t){return qt(t,this._week.dow,this._week.doy).week},Dn.firstDayOfYear=function(){return this._week.doy},Dn.firstDayOfWeek=function(){return this._week.dow},Dn.weekdays=function(t,e){var n=r(this._weekdays)?this._weekdays:this._weekdays[t&&!0!==t&&this._weekdays.isFormat.test(e)?"format":"standalone"];return!0===t?$t(n,this._week.dow):t?n[t.day()]:n},Dn.weekdaysMin=function(t){return!0===t?$t(this._weekdaysMin,this._week.dow):t?this._weekdaysMin[t.day()]:this._weekdaysMin},Dn.weekdaysShort=function(t){return!0===t?$t(this._weekdaysShort,this._week.dow):t?this._weekdaysShort[t.day()]:this._weekdaysShort},Dn.weekdaysParse=function(t,e,n){var i,a,r;if(this._weekdaysParseExact)return Qt.call(this,t,e,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),i=0;i<7;i++){if(a=f([2e3,1]).day(i),n&&!this._fullWeekdaysParse[i]&&(this._fullWeekdaysParse[i]=new RegExp("^"+this.weekdays(a,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[i]=new RegExp("^"+this.weekdaysShort(a,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[i]=new RegExp("^"+this.weekdaysMin(a,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[i]||(r="^"+this.weekdays(a,"")+"|^"+this.weekdaysShort(a,"")+"|^"+this.weekdaysMin(a,""),this._weekdaysParse[i]=new RegExp(r.replace(".",""),"i")),n&&"dddd"===e&&this._fullWeekdaysParse[i].test(t))return i;if(n&&"ddd"===e&&this._shortWeekdaysParse[i].test(t))return i;if(n&&"dd"===e&&this._minWeekdaysParse[i].test(t))return i;if(!n&&this._weekdaysParse[i].test(t))return i}},Dn.weekdaysRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(h(this,"_weekdaysRegex")||(this._weekdaysRegex=te),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)},Dn.weekdaysShortRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(h(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ee),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},Dn.weekdaysMinRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(h(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=ne),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},Dn.isPM=function(t){return"p"===(t+"").toLowerCase().charAt(0)},Dn.meridiem=function(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"},ge("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10;return t+(1===k(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th")}}),a.lang=D("moment.lang is deprecated. Use moment.locale instead.",ge),a.langData=D("moment.langData is deprecated. Use moment.localeData instead.",pe);var On=Math.abs;function An(t,e,n,i){var a=Xe(e,n);return t._milliseconds+=i*a._milliseconds,t._days+=i*a._days,t._months+=i*a._months,t._bubble()}function Fn(t){return t<0?Math.floor(t):Math.ceil(t)}function In(t){return 4800*t/146097}function Ln(t){return 146097*t/4800}function Rn(t){return function(){return this.as(t)}}var Nn=Rn("ms"),Wn=Rn("s"),Yn=Rn("m"),zn=Rn("h"),En=Rn("d"),Vn=Rn("w"),Hn=Rn("M"),Bn=Rn("Q"),jn=Rn("y");function Un(t){return function(){return this.isValid()?this._data[t]:NaN}}var Gn=Un("milliseconds"),qn=Un("seconds"),Zn=Un("minutes"),$n=Un("hours"),Xn=Un("days"),Kn=Un("months"),Jn=Un("years"),Qn=Math.round,ti={ss:44,s:45,m:45,h:22,d:26,M:11};function ei(t,e,n,i,a){return a.relativeTime(e||1,!!n,t,i)}var ni=Math.abs;function ii(t){return(t>0)-(t<0)||+t}function ai(){if(!this.isValid())return this.localeData().invalidDate();var t,e,n=ni(this._milliseconds)/1e3,i=ni(this._days),a=ni(this._months);t=w(n/60),e=w(t/60),n%=60,t%=60;var r=w(a/12),o=a%=12,s=i,l=e,u=t,d=n?n.toFixed(3).replace(/\.?0+$/,""):"",h=this.asSeconds();if(!h)return"P0D";var c=h<0?"-":"",f=ii(this._months)!==ii(h)?"-":"",g=ii(this._days)!==ii(h)?"-":"",m=ii(this._milliseconds)!==ii(h)?"-":"";return c+"P"+(r?f+r+"Y":"")+(o?f+o+"M":"")+(s?g+s+"D":"")+(l||u||d?"T":"")+(l?m+l+"H":"")+(u?m+u+"M":"")+(d?m+d+"S":"")}var ri=ze.prototype;return ri.isValid=function(){return this._isValid},ri.abs=function(){var t=this._data;return this._milliseconds=On(this._milliseconds),this._days=On(this._days),this._months=On(this._months),t.milliseconds=On(t.milliseconds),t.seconds=On(t.seconds),t.minutes=On(t.minutes),t.hours=On(t.hours),t.months=On(t.months),t.years=On(t.years),this},ri.add=function(t,e){return An(this,t,e,1)},ri.subtract=function(t,e){return An(this,t,e,-1)},ri.as=function(t){if(!this.isValid())return NaN;var e,n,i=this._milliseconds;if("month"===(t=R(t))||"quarter"===t||"year"===t)switch(e=this._days+i/864e5,n=this._months+In(e),t){case"month":return n;case"quarter":return n/3;case"year":return n/12}else switch(e=this._days+Math.round(Ln(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)}},ri.asMilliseconds=Nn,ri.asSeconds=Wn,ri.asMinutes=Yn,ri.asHours=zn,ri.asDays=En,ri.asWeeks=Vn,ri.asMonths=Hn,ri.asQuarters=Bn,ri.asYears=jn,ri.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*k(this._months/12):NaN},ri._bubble=function(){var t,e,n,i,a,r=this._milliseconds,o=this._days,s=this._months,l=this._data;return r>=0&&o>=0&&s>=0||r<=0&&o<=0&&s<=0||(r+=864e5*Fn(Ln(s)+o),o=0,s=0),l.milliseconds=r%1e3,t=w(r/1e3),l.seconds=t%60,e=w(t/60),l.minutes=e%60,n=w(e/60),l.hours=n%24,o+=w(n/24),a=w(In(o)),s+=a,o-=Fn(Ln(a)),i=w(s/12),s%=12,l.days=o,l.months=s,l.years=i,this},ri.clone=function(){return Xe(this)},ri.get=function(t){return t=R(t),this.isValid()?this[t+"s"]():NaN},ri.milliseconds=Gn,ri.seconds=qn,ri.minutes=Zn,ri.hours=$n,ri.days=Xn,ri.weeks=function(){return w(this.days()/7)},ri.months=Kn,ri.years=Jn,ri.humanize=function(t){if(!this.isValid())return this.localeData().invalidDate();var e=this.localeData(),n=function(t,e,n){var i=Xe(t).abs(),a=Qn(i.as("s")),r=Qn(i.as("m")),o=Qn(i.as("h")),s=Qn(i.as("d")),l=Qn(i.as("M")),u=Qn(i.as("y")),d=a<=ti.ss&&["s",a]||a<ti.s&&["ss",a]||r<=1&&["m"]||r<ti.m&&["mm",r]||o<=1&&["h"]||o<ti.h&&["hh",o]||s<=1&&["d"]||s<ti.d&&["dd",s]||l<=1&&["M"]||l<ti.M&&["MM",l]||u<=1&&["y"]||["yy",u];return d[2]=e,d[3]=+t>0,d[4]=n,ei.apply(null,d)}(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)},ri.toISOString=ai,ri.toString=ai,ri.toJSON=ai,ri.locale=rn,ri.localeData=sn,ri.toIsoString=D("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",ai),ri.lang=on,j("X",0,0,"unix"),j("x",0,0,"valueOf"),dt("x",rt),dt("X",/[+-]?\d+(\.\d{1,3})?/),gt("X",(function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))})),gt("x",(function(t,e,n){n._d=new Date(k(t))})),a.version="2.24.0",n=Le,a.fn=Mn,a.min=function(){return We("isBefore",[].slice.call(arguments,0))},a.max=function(){return We("isAfter",[].slice.call(arguments,0))},a.now=function(){return Date.now?Date.now():+new Date},a.utc=f,a.unix=function(t){return Le(1e3*t)},a.months=function(t,e){return Pn(t,e,"months")},a.isDate=u,a.locale=ge,a.invalid=p,a.duration=Xe,a.isMoment=_,a.weekdays=function(t,e,n){return Tn(t,e,n,"weekdays")},a.parseZone=function(){return Le.apply(null,arguments).parseZone()},a.localeData=pe,a.isDuration=Ee,a.monthsShort=function(t,e){return Pn(t,e,"monthsShort")},a.weekdaysMin=function(t,e,n){return Tn(t,e,n,"weekdaysMin")},a.defineLocale=me,a.updateLocale=function(t,e){if(null!=e){var n,i,a=ue;null!=(i=fe(t))&&(a=i._config),e=A(a,e),(n=new F(e)).parentLocale=de[t],de[t]=n,ge(t)}else null!=de[t]&&(null!=de[t].parentLocale?de[t]=de[t].parentLocale:null!=de[t]&&delete de[t]);return de[t]},a.locales=function(){return C(de)},a.weekdaysShort=function(t,e,n){return Tn(t,e,n,"weekdaysShort")},a.normalizeUnits=R,a.relativeTimeRounding=function(t){return void 0===t?Qn:"function"==typeof t&&(Qn=t,!0)},a.relativeTimeThreshold=function(t,e){return void 0!==ti[t]&&(void 0===e?ti[t]:(ti[t]=e,"s"===t&&(ti.ss=e-1),!0))},a.calendarFormat=function(t,e){var n=t.diff(e,"days",!0);return n<-6?"sameElse":n<-1?"lastWeek":n<0?"lastDay":n<1?"sameDay":n<2?"nextDay":n<7?"nextWeek":"sameElse"},a.prototype=Mn,a.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},a}()})),pi={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};on._date.override("function"==typeof mi?{_id:"moment",formats:function(){return pi},parse:function(t,e){return"string"==typeof t&&"string"==typeof e?t=mi(t,e):t instanceof mi||(t=mi(t)),t.isValid()?t.valueOf():null},format:function(t,e){return mi(t).format(e)},add:function(t,e,n){return mi(t).add(e,n).valueOf()},diff:function(t,e,n){return mi(t).diff(mi(e),n)},startOf:function(t,e,n){return t=mi(t),"isoWeek"===e?t.isoWeekday(n).valueOf():t.startOf(e).valueOf()},endOf:function(t,e){return mi(t).endOf(e).valueOf()},_create:function(t){return mi(t)}}:{}),Y._set("global",{plugins:{filler:{propagate:!0}}});var vi={dataset:function(t){var e=t.fill,n=t.chart,i=n.getDatasetMeta(e),a=i&&n.isDatasetVisible(e)&&i.dataset._children||[],r=a.length||0;return r?function(t,e){return e<r&&a[e]._view||null}:null},boundary:function(t){var e=t.boundary,n=e?e.x:null,i=e?e.y:null;return B.isArray(e)?function(t,n){return e[n]}:function(t){return{x:null===n?t.x:n,y:null===i?t.y:i}}}};function bi(t,e,n){var i,a=t._model||{},r=a.fill;if(void 0===r&&(r=!!a.backgroundColor),!1===r||null===r)return!1;if(!0===r)return"origin";if(i=parseFloat(r,10),isFinite(i)&&Math.floor(i)===i)return"-"!==r[0]&&"+"!==r[0]||(i=e+i),!(i===e||i<0||i>=n)&&i;switch(r){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return r;default:return!1}}function yi(t){return(t.el._scale||{}).getPointPositionForValue?function(t){var e,n,i,a,r,o=t.el._scale,s=o.options,l=o.chart.data.labels.length,u=t.fill,d=[];if(!l)return null;for(e=s.ticks.reverse?o.max:o.min,n=s.ticks.reverse?o.min:o.max,i=o.getPointPositionForValue(0,e),a=0;a<l;++a)r="start"===u||"end"===u?o.getPointPositionForValue(a,"start"===u?e:n):o.getBasePosition(a),s.gridLines.circular&&(r.cx=i.x,r.cy=i.y,r.angle=o.getIndexAngle(a)-Math.PI/2),d.push(r);return d}(t):function(t){var e,n=t.el._model||{},i=t.el._scale||{},a=t.fill,r=null;if(isFinite(a))return null;if("start"===a?r=void 0===n.scaleBottom?i.bottom:n.scaleBottom:"end"===a?r=void 0===n.scaleTop?i.top:n.scaleTop:void 0!==n.scaleZero?r=n.scaleZero:i.getBasePixel&&(r=i.getBasePixel()),null!=r){if(void 0!==r.x&&void 0!==r.y)return r;if(B.isFinite(r))return{x:(e=i.isHorizontal())?r:null,y:e?null:r}}return null}(t)}function xi(t,e,n){var i,a=t[e].fill,r=[e];if(!n)return a;for(;!1!==a&&-1===r.indexOf(a);){if(!isFinite(a))return a;if(!(i=t[a]))return!1;if(i.visible)return a;r.push(a),a=i.fill}return!1}function _i(t){var e=t.fill,n="dataset";return!1===e?null:(isFinite(e)||(n="boundary"),vi[n](t))}function wi(t){return t&&!t.skip}function ki(t,e,n,i,a){var r,o,s,l;if(i&&a){for(t.moveTo(e[0].x,e[0].y),r=1;r<i;++r)B.canvas.lineTo(t,e[r-1],e[r]);if(void 0===n[0].angle)for(t.lineTo(n[a-1].x,n[a-1].y),r=a-1;r>0;--r)B.canvas.lineTo(t,n[r],n[r-1],!0);else for(o=n[0].cx,s=n[0].cy,l=Math.sqrt(Math.pow(n[0].x-o,2)+Math.pow(n[0].y-s,2)),r=a-1;r>0;--r)t.arc(o,s,l,n[r].angle,n[r-1].angle,!0)}}function Mi(t,e,n,i,a,r){var o,s,l,u,d,h,c,f,g=e.length,m=i.spanGaps,p=[],v=[],b=0,y=0;for(t.beginPath(),o=0,s=g;o<s;++o)d=n(u=e[l=o%g]._view,l,i),h=wi(u),c=wi(d),r&&void 0===f&&h&&(s=g+(f=o+1)),h&&c?(b=p.push(u),y=v.push(d)):b&&y&&(m?(h&&p.push(u),c&&v.push(d)):(ki(t,p,v,b,y),b=y=0,p=[],v=[]));ki(t,p,v,b,y),t.closePath(),t.fillStyle=a,t.fill()}var Si={id:"filler",afterDatasetsUpdate:function(t,e){var n,i,a,r,o=(t.data.datasets||[]).length,s=e.propagate,l=[];for(i=0;i<o;++i)r=null,(a=(n=t.getDatasetMeta(i)).dataset)&&a._model&&a instanceof kt.Line&&(r={visible:t.isDatasetVisible(i),fill:bi(a,i,o),chart:t,el:a}),n.$filler=r,l.push(r);for(i=0;i<o;++i)(r=l[i])&&(r.fill=xi(l,i,s),r.boundary=yi(r),r.mapper=_i(r))},beforeDatasetsDraw:function(t){var e,n,i,a,r,o,s,l=t._getSortedVisibleDatasetMetas(),u=t.ctx;for(n=l.length-1;n>=0;--n)(e=l[n].$filler)&&e.visible&&(a=(i=e.el)._view,r=i._children||[],o=e.mapper,s=a.backgroundColor||Y.global.defaultColor,o&&s&&r.length&&(B.canvas.clipArea(u,t.chartArea),Mi(u,r,o,a,s,i._loop),B.canvas.unclipArea(u)))}},Di=B.rtl.getRtlAdapter,Ci=B.noop,Pi=B.valueOrDefault;function Ti(t,e){return t.usePointStyle&&t.boxWidth>e?e:t.boxWidth}Y._set("global",{legend:{display:!0,position:"top",align:"center",fullWidth:!0,reverse:!1,weight:1e3,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,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data.datasets,n=t.options.legend||{},i=n.labels&&n.labels.usePointStyle;return t._getSortedDatasetMetas().map((function(n){var a=n.controller.getStyle(i?0:void 0);return{text:e[n.index].label,fillStyle:a.backgroundColor,hidden:!t.isDatasetVisible(n.index),lineCap:a.borderCapStyle,lineDash:a.borderDash,lineDashOffset:a.borderDashOffset,lineJoin:a.borderJoinStyle,lineWidth:a.borderWidth,strokeStyle:a.borderColor,pointStyle:a.pointStyle,rotation:a.rotation,datasetIndex:n.index}}),this)}}},legendCallback:function(t){var e,n,i,a=document.createElement("ul"),r=t.data.datasets;for(a.setAttribute("class",t.id+"-legend"),e=0,n=r.length;e<n;e++)(i=a.appendChild(document.createElement("li"))).appendChild(document.createElement("span")).style.backgroundColor=r[e].backgroundColor,r[e].label&&i.appendChild(document.createTextNode(r[e].label));return a.outerHTML}});var Oi=X.extend({initialize:function(t){B.extend(this,t),this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1},beforeUpdate:Ci,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:Ci,beforeSetDimensions:Ci,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:Ci,beforeBuildLabels:Ci,buildLabels:function(){var t=this,e=t.options.labels||{},n=B.callback(e.generateLabels,[t.chart],t)||[];e.filter&&(n=n.filter((function(n){return e.filter(n,t.chart.data)}))),t.options.reverse&&n.reverse(),t.legendItems=n},afterBuildLabels:Ci,beforeFit:Ci,fit:function(){var t=this,e=t.options,n=e.labels,i=e.display,a=t.ctx,r=B.options._parseFont(n),o=r.size,s=t.legendHitBoxes=[],l=t.minSize,u=t.isHorizontal();if(u?(l.width=t.maxWidth,l.height=i?10:0):(l.width=i?10:0,l.height=t.maxHeight),i){if(a.font=r.string,u){var d=t.lineWidths=[0],h=0;a.textAlign="left",a.textBaseline="middle",B.each(t.legendItems,(function(t,e){var i=Ti(n,o)+o/2+a.measureText(t.text).width;(0===e||d[d.length-1]+i+2*n.padding>l.width)&&(h+=o+n.padding,d[d.length-(e>0?0:1)]=0),s[e]={left:0,top:0,width:i,height:o},d[d.length-1]+=i+n.padding})),l.height+=h}else{var c=n.padding,f=t.columnWidths=[],g=t.columnHeights=[],m=n.padding,p=0,v=0;B.each(t.legendItems,(function(t,e){var i=Ti(n,o)+o/2+a.measureText(t.text).width;e>0&&v+o+2*c>l.height&&(m+=p+n.padding,f.push(p),g.push(v),p=0,v=0),p=Math.max(p,i),v+=o+c,s[e]={left:0,top:0,width:i,height:o}})),m+=p,f.push(p),g.push(v),l.width+=m}t.width=l.width,t.height=l.height}else t.width=l.width=t.height=l.height=0},afterFit:Ci,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var t=this,e=t.options,n=e.labels,i=Y.global,a=i.defaultColor,r=i.elements.line,o=t.height,s=t.columnHeights,l=t.width,u=t.lineWidths;if(e.display){var d,h=Di(e.rtl,t.left,t.minSize.width),c=t.ctx,f=Pi(n.fontColor,i.defaultFontColor),g=B.options._parseFont(n),m=g.size;c.textAlign=h.textAlign("left"),c.textBaseline="middle",c.lineWidth=.5,c.strokeStyle=f,c.fillStyle=f,c.font=g.string;var p=Ti(n,m),v=t.legendHitBoxes,b=function(t,i){switch(e.align){case"start":return n.padding;case"end":return t-i;default:return(t-i+n.padding)/2}},y=t.isHorizontal();d=y?{x:t.left+b(l,u[0]),y:t.top+n.padding,line:0}:{x:t.left+n.padding,y:t.top+b(o,s[0]),line:0},B.rtl.overrideTextDirection(t.ctx,e.textDirection);var x=m+n.padding;B.each(t.legendItems,(function(e,i){var f=c.measureText(e.text).width,g=p+m/2+f,_=d.x,w=d.y;h.setWidth(t.minSize.width),y?i>0&&_+g+n.padding>t.left+t.minSize.width&&(w=d.y+=x,d.line++,_=d.x=t.left+b(l,u[d.line])):i>0&&w+x>t.top+t.minSize.height&&(_=d.x=_+t.columnWidths[d.line]+n.padding,d.line++,w=d.y=t.top+b(o,s[d.line]));var k=h.x(_);!function(t,e,i){if(!(isNaN(p)||p<=0)){c.save();var o=Pi(i.lineWidth,r.borderWidth);if(c.fillStyle=Pi(i.fillStyle,a),c.lineCap=Pi(i.lineCap,r.borderCapStyle),c.lineDashOffset=Pi(i.lineDashOffset,r.borderDashOffset),c.lineJoin=Pi(i.lineJoin,r.borderJoinStyle),c.lineWidth=o,c.strokeStyle=Pi(i.strokeStyle,a),c.setLineDash&&c.setLineDash(Pi(i.lineDash,r.borderDash)),n&&n.usePointStyle){var s=p*Math.SQRT2/2,l=h.xPlus(t,p/2),u=e+m/2;B.canvas.drawPoint(c,i.pointStyle,s,l,u,i.rotation)}else c.fillRect(h.leftForLtr(t,p),e,p,m),0!==o&&c.strokeRect(h.leftForLtr(t,p),e,p,m);c.restore()}}(k,w,e),v[i].left=h.leftForLtr(k,v[i].width),v[i].top=w,function(t,e,n,i){var a=m/2,r=h.xPlus(t,p+a),o=e+a;c.fillText(n.text,r,o),n.hidden&&(c.beginPath(),c.lineWidth=2,c.moveTo(r,o),c.lineTo(h.xPlus(r,i),o),c.stroke())}(k,w,e,f),y?d.x+=g+n.padding:d.y+=x})),B.rtl.restoreTextDirection(t.ctx,e.textDirection)}},_getLegendItemAt:function(t,e){var n,i,a,r=this;if(t>=r.left&&t<=r.right&&e>=r.top&&e<=r.bottom)for(a=r.legendHitBoxes,n=0;n<a.length;++n)if(t>=(i=a[n]).left&&t<=i.left+i.width&&e>=i.top&&e<=i.top+i.height)return r.legendItems[n];return null},handleEvent:function(t){var e,n=this,i=n.options,a="mouseup"===t.type?"click":t.type;if("mousemove"===a){if(!i.onHover&&!i.onLeave)return}else{if("click"!==a)return;if(!i.onClick)return}e=n._getLegendItemAt(t.x,t.y),"click"===a?e&&i.onClick&&i.onClick.call(n,t.native,e):(i.onLeave&&e!==n._hoveredItem&&(n._hoveredItem&&i.onLeave.call(n,t.native,n._hoveredItem),n._hoveredItem=e),i.onHover&&e&&i.onHover.call(n,t.native,e))}});function Ai(t,e){var n=new Oi({ctx:t.ctx,options:e,chart:t});pe.configure(t,n,e),pe.addBox(t,n),t.legend=n}var Fi={id:"legend",_element:Oi,beforeInit:function(t){var e=t.options.legend;e&&Ai(t,e)},beforeUpdate:function(t){var e=t.options.legend,n=t.legend;e?(B.mergeIf(e,Y.global.legend),n?(pe.configure(t,n,e),n.options=e):Ai(t,e)):n&&(pe.removeBox(t,n),delete t.legend)},afterEvent:function(t,e){var n=t.legend;n&&n.handleEvent(e)}},Ii=B.noop;Y._set("global",{title:{display:!1,fontStyle:"bold",fullWidth:!0,padding:10,position:"top",text:"",weight:2e3}});var Li=X.extend({initialize:function(t){B.extend(this,t),this.legendHitBoxes=[]},beforeUpdate:Ii,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:Ii,beforeSetDimensions:Ii,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:Ii,beforeBuildLabels:Ii,buildLabels:Ii,afterBuildLabels:Ii,beforeFit:Ii,fit:function(){var t,e=this,n=e.options,i=e.minSize={},a=e.isHorizontal();n.display?(t=(B.isArray(n.text)?n.text.length:1)*B.options._parseFont(n).lineHeight+2*n.padding,e.width=i.width=a?e.maxWidth:t,e.height=i.height=a?t:e.maxHeight):e.width=i.width=e.height=i.height=0},afterFit:Ii,isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},draw:function(){var t=this,e=t.ctx,n=t.options;if(n.display){var i,a,r,o=B.options._parseFont(n),s=o.lineHeight,l=s/2+n.padding,u=0,d=t.top,h=t.left,c=t.bottom,f=t.right;e.fillStyle=B.valueOrDefault(n.fontColor,Y.global.defaultFontColor),e.font=o.string,t.isHorizontal()?(a=h+(f-h)/2,r=d+l,i=f-h):(a="left"===n.position?h+l:f-l,r=d+(c-d)/2,i=c-d,u=Math.PI*("left"===n.position?-.5:.5)),e.save(),e.translate(a,r),e.rotate(u),e.textAlign="center",e.textBaseline="middle";var g=n.text;if(B.isArray(g))for(var m=0,p=0;p<g.length;++p)e.fillText(g[p],0,m,i),m+=s;else e.fillText(g,0,0,i);e.restore()}}});function Ri(t,e){var n=new Li({ctx:t.ctx,options:e,chart:t});pe.configure(t,n,e),pe.addBox(t,n),t.titleBlock=n}var Ni={},Wi=Si,Yi=Fi,zi={id:"title",_element:Li,beforeInit:function(t){var e=t.options.title;e&&Ri(t,e)},beforeUpdate:function(t){var e=t.options.title,n=t.titleBlock;e?(B.mergeIf(e,Y.global.title),n?(pe.configure(t,n,e),n.options=e):Ri(t,e)):n&&(pe.removeBox(t,n),delete t.titleBlock)}};for(var Ei in Ni.filler=Wi,Ni.legend=Yi,Ni.title=zi,nn.helpers=B,function(){function t(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 e(t){return null!=t&&"none"!==t}function n(n,i,a){var r=document.defaultView,o=B._getParentNode(n),s=r.getComputedStyle(n)[i],l=r.getComputedStyle(o)[i],u=e(s),d=e(l),h=Number.POSITIVE_INFINITY;return u||d?Math.min(u?t(s,n,a):h,d?t(l,o,a):h):"none"}B.where=function(t,e){if(B.isArray(t)&&Array.prototype.filter)return t.filter(e);var n=[];return B.each(t,(function(t){e(t)&&n.push(t)})),n},B.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;i<a;++i)if(e.call(n,t[i],i,t))return i;return-1},B.findNextWhere=function(t,e,n){B.isNullOrUndef(n)&&(n=-1);for(var i=n+1;i<t.length;i++){var a=t[i];if(e(a))return a}},B.findPreviousWhere=function(t,e,n){B.isNullOrUndef(n)&&(n=t.length);for(var i=n-1;i>=0;i--){var a=t[i];if(e(a))return a}},B.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},B.almostEquals=function(t,e,n){return Math.abs(t-e)<n},B.almostWhole=function(t,e){var n=Math.round(t);return n-e<=t&&n+e>=t},B.max=function(t){return t.reduce((function(t,e){return isNaN(e)?t:Math.max(t,e)}),Number.NEGATIVE_INFINITY)},B.min=function(t){return t.reduce((function(t,e){return isNaN(e)?t:Math.min(t,e)}),Number.POSITIVE_INFINITY)},B.sign=Math.sign?function(t){return Math.sign(t)}:function(t){return 0===(t=+t)||isNaN(t)?t:t>0?1:-1},B.toRadians=function(t){return t*(Math.PI/180)},B.toDegrees=function(t){return t*(180/Math.PI)},B._decimalPlaces=function(t){if(B.isFinite(t)){for(var e=1,n=0;Math.round(t*e)/e!==t;)e*=10,n++;return n}},B.getAngleFromPoint=function(t,e){var n=e.x-t.x,i=e.y-t.y,a=Math.sqrt(n*n+i*i),r=Math.atan2(i,n);return r<-.5*Math.PI&&(r+=2*Math.PI),{angle:r,distance:a}},B.distanceBetweenPoints=function(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))},B.aliasPixel=function(t){return t%2==0?0:.5},B._alignPixel=function(t,e,n){var i=t.currentDevicePixelRatio,a=n/2;return Math.round((e-a)*i)/i+a},B.splineCurve=function(t,e,n,i){var a=t.skip?e:t,r=e,o=n.skip?e:n,s=Math.sqrt(Math.pow(r.x-a.x,2)+Math.pow(r.y-a.y,2)),l=Math.sqrt(Math.pow(o.x-r.x,2)+Math.pow(o.y-r.y,2)),u=s/(s+l),d=l/(s+l),h=i*(u=isNaN(u)?0:u),c=i*(d=isNaN(d)?0:d);return{previous:{x:r.x-h*(o.x-a.x),y:r.y-h*(o.y-a.y)},next:{x:r.x+c*(o.x-a.x),y:r.y+c*(o.y-a.y)}}},B.EPSILON=Number.EPSILON||1e-14,B.splineCurveMonotone=function(t){var e,n,i,a,r,o,s,l,u,d=(t||[]).map((function(t){return{model:t._model,deltaK:0,mK:0}})),h=d.length;for(e=0;e<h;++e)if(!(i=d[e]).model.skip){if(n=e>0?d[e-1]:null,(a=e<h-1?d[e+1]:null)&&!a.model.skip){var c=a.model.x-i.model.x;i.deltaK=0!==c?(a.model.y-i.model.y)/c:0}!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}for(e=0;e<h-1;++e)i=d[e],a=d[e+1],i.model.skip||a.model.skip||(B.almostEquals(i.deltaK,0,this.EPSILON)?i.mK=a.mK=0:(r=i.mK/i.deltaK,o=a.mK/i.deltaK,(l=Math.pow(r,2)+Math.pow(o,2))<=9||(s=3/Math.sqrt(l),i.mK=r*s*i.deltaK,a.mK=o*s*i.deltaK)));for(e=0;e<h;++e)(i=d[e]).model.skip||(n=e>0?d[e-1]:null,a=e<h-1?d[e+1]:null,n&&!n.model.skip&&(u=(i.model.x-n.model.x)/3,i.model.controlPointPreviousX=i.model.x-u,i.model.controlPointPreviousY=i.model.y-u*i.mK),a&&!a.model.skip&&(u=(a.model.x-i.model.x)/3,i.model.controlPointNextX=i.model.x+u,i.model.controlPointNextY=i.model.y+u*i.mK))},B.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]},B.previousItem=function(t,e,n){return n?e<=0?t[t.length-1]:t[e-1]:e<=0?t[0]:t[e-1]},B.niceNum=function(t,e){var n=Math.floor(B.log10(t)),i=t/Math.pow(10,n);return(e?i<1.5?1:i<3?2:i<7?5:10:i<=1?1:i<=2?2:i<=5?5:10)*Math.pow(10,n)},B.requestAnimFrame="undefined"==typeof window?function(t){t()}:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)},B.getRelativePosition=function(t,e){var n,i,a=t.originalEvent||t,r=t.target||t.srcElement,o=r.getBoundingClientRect(),s=a.touches;s&&s.length>0?(n=s[0].clientX,i=s[0].clientY):(n=a.clientX,i=a.clientY);var l=parseFloat(B.getStyle(r,"padding-left")),u=parseFloat(B.getStyle(r,"padding-top")),d=parseFloat(B.getStyle(r,"padding-right")),h=parseFloat(B.getStyle(r,"padding-bottom")),c=o.right-o.left-l-d,f=o.bottom-o.top-u-h;return{x:n=Math.round((n-o.left-l)/c*r.width/e.currentDevicePixelRatio),y:i=Math.round((i-o.top-u)/f*r.height/e.currentDevicePixelRatio)}},B.getConstraintWidth=function(t){return n(t,"max-width","clientWidth")},B.getConstraintHeight=function(t){return n(t,"max-height","clientHeight")},B._calculatePadding=function(t,e,n){return(e=B.getStyle(t,e)).indexOf("%")>-1?n*parseInt(e,10)/100:parseInt(e,10)},B._getParentNode=function(t){var e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e},B.getMaximumWidth=function(t){var e=B._getParentNode(t);if(!e)return t.clientWidth;var n=e.clientWidth,i=n-B._calculatePadding(e,"padding-left",n)-B._calculatePadding(e,"padding-right",n),a=B.getConstraintWidth(t);return isNaN(a)?i:Math.min(i,a)},B.getMaximumHeight=function(t){var e=B._getParentNode(t);if(!e)return t.clientHeight;var n=e.clientHeight,i=n-B._calculatePadding(e,"padding-top",n)-B._calculatePadding(e,"padding-bottom",n),a=B.getConstraintHeight(t);return isNaN(a)?i:Math.min(i,a)},B.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},B.retinaScale=function(t,e){var n=t.currentDevicePixelRatio=e||"undefined"!=typeof window&&window.devicePixelRatio||1;if(1!==n){var i=t.canvas,a=t.height,r=t.width;i.height=a*n,i.width=r*n,t.ctx.scale(n,n),i.style.height||i.style.width||(i.style.height=a+"px",i.style.width=r+"px")}},B.fontString=function(t,e,n){return e+" "+t+"px "+n},B.longestText=function(t,e,n,i){var a=(i=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 o,s,l,u,d,h=0,c=n.length;for(o=0;o<c;o++)if(null!=(u=n[o])&&!0!==B.isArray(u))h=B.measureText(t,a,r,h,u);else if(B.isArray(u))for(s=0,l=u.length;s<l;s++)null==(d=u[s])||B.isArray(d)||(h=B.measureText(t,a,r,h,d));var f=r.length/2;if(f>n.length){for(o=0;o<f;o++)delete a[r[o]];r.splice(0,f)}return h},B.measureText=function(t,e,n,i,a){var r=e[a];return r||(r=e[a]=t.measureText(a).width,n.push(a)),r>i&&(i=r),i},B.numberOfLabelLines=function(t){var e=1;return B.each(t,(function(t){B.isArray(t)&&t.length>e&&(e=t.length)})),e},B.color=w?function(t){return t instanceof CanvasGradient&&(t=Y.global.defaultColor),w(t)}:function(t){return console.error("Color.js not found!"),t},B.getHoverColor=function(t){return t instanceof CanvasPattern||t instanceof CanvasGradient?t:B.color(t).saturate(.5).darken(.1).rgbString()}}(),nn._adapters=on,nn.Animation=J,nn.animationService=Q,nn.controllers=Qt,nn.DatasetController=at,nn.defaults=Y,nn.Element=X,nn.elements=kt,nn.Interaction=oe,nn.layouts=pe,nn.platform=Le,nn.plugins=Re,nn.Scale=_n,nn.scaleService=Ne,nn.Ticks=sn,nn.Tooltip=qe,nn.helpers.each(gi,(function(t,e){nn.scaleService.registerScaleType(e,t,t._defaults)})),Ni)Ni.hasOwnProperty(Ei)&&nn.plugins.register(Ni[Ei]);nn.platform.initialize();var Vi=nn;return"undefined"!=typeof window&&(window.Chart=nn),nn.Chart=nn,nn.Legend=Ni.legend._element,nn.Title=Ni.title._element,nn.pluginService=nn.plugins,nn.PluginBase=nn.Element.extend({}),nn.canvasHelpers=nn.helpers.canvas,nn.layoutService=nn.layouts,nn.LinearScaleBase=Cn,nn.helpers.each(["Bar","Bubble","Doughnut","Line","PolarArea","Radar","Scatter"],(function(t){nn[t]=function(e,n){return new nn(e,nn.helpers.merge(n||{},{type:t.charAt(0).toLowerCase()+t.slice(1)}))}})),Vi}));
diff --git a/public/images/icon.png b/assets/icon.png
index 51ca51c..51ca51c 100644
--- a/public/images/icon.png
+++ b/assets/icon.png
Binary files differ
diff --git a/assets/main.css b/assets/main.css
new file mode 100644
index 0000000..ba43f85
--- /dev/null
+++ b/assets/main.css
@@ -0,0 +1,399 @@
+/* Constants */
+
+:root {
+ /* Colors */
+ --color-red: #CF5C56;
+ --color-brown: #A04642;
+ --color-green: #9FD2A5;
+ --color-blue: #6CA2A4;
+ --color-gray: #EEEEEE;
+ --color-yellow: #FFF09E;
+
+ /* Sizes */
+ --size-bee: 0.2rem;
+ --size-mouse: 0.5rem;
+ --size-cat: 0.75rem;
+ --size-dog: 1rem;
+ --size-lion: 1.25rem;
+ --size-bear: 1.5rem;
+ --size-horse: 2rem;
+ --size-camel: 2.625rem;
+ --size-rhino: 3.375rem;
+ --size-elephant: 4.25rem;
+ --size-mammoth: 5.25rem;
+}
+
+/* Reset */
+
+html { box-sizing: border-box; }
+*, *:before, *:after { box-sizing: inherit; }
+body { margin: 0; }
+p { margin: 0; }
+a { color: inherit; text-decoration: none; }
+h1 { font-size: inherit; font-weight: normal; margin: 0; }
+select { height: inherit; font-size: inherit; }
+input { color: inherit; font-size: inherit; }
+ul { list-style-type: none; margin: 0; padding-left: 0; }
+
+/* Media */
+
+@media screen and (max-width: 500px) {
+ html { font-size: 14px; }
+ .g-Media__Large {
+ display: none !important;
+ }
+}
+
+@media screen and (min-width: 500px) and (max-width: 900px) {
+ html { font-size: 16px; }
+}
+
+@media screen and (min-width: 900px) {
+ html { font-size: 18px; }
+}
+
+/* Layout */
+
+body {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.g-Header {
+ width: 100%;
+}
+
+.g-Header__Primary {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--size-cat) var(--size-dog);
+}
+
+.g-Header__Title {
+ display: block;
+ font-size: var(--size-horse);
+ color: var(--color-red);
+}
+
+.g-Header__Logout {
+ cursor: pointer;
+ margin-left: var(--size-cat);
+ background-color: transparent;
+ border: none;
+}
+
+.g-Header__Logout:hover {
+ text-decoration: underline;
+}
+
+.g-Header__Logout:focus {
+ text-decoration: underline;
+}
+
+.g-Header__Secondary {
+ display: flex;
+ background-color: var(--color-red);
+ color: white;
+ overflow-y: auto;
+}
+
+.g-Header__Link {
+ padding: var(--size-mouse) var(--size-dog);
+}
+
+.g-Header__Link:hover {
+ background-color: var(--color-brown);
+}
+
+.g-Header__Link:focus {
+ background-color: var(--color-brown);
+}
+
+.g-Header__Link--Current {
+ background-color: var(--color-brown);
+}
+
+.g-Main {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ padding: 0 var(--size-bear);
+ margin: var(--size-horse) 0;
+ overflow-x: hidden;
+}
+
+.g-Title {
+ font-size: var(--size-lion);
+ color: var(--color-red);
+ margin-bottom: var(--size-lion);
+ text-decoration: underline;
+}
+
+.g-Paragraph {
+ margin-bottom: var(--size-lion);
+}
+
+/* Heading */
+
+.g-H1 {
+ font-size: var(--size-lion);
+ text-decoration: underline;
+ margin-bottom: var(--size-lion);
+}
+
+/* Link */
+
+.g-Link {
+ color: var(--color-blue);
+}
+
+.g-Link:hover {
+ text-decoration: underline;
+}
+
+/* Table */
+
+.g-Table {
+ display: table;
+ margin: 0 auto var(--size-bear);
+}
+
+.g-Table__Row {
+ display: table-row;
+ text-decoration: none;
+ color: inherit;
+}
+
+.g-Table__Row--Highlight {
+ background-color: var(--color-yellow);
+}
+
+.g-Table__Row:not(.g-Table__Row--Header):not(.g-Table__Row--Highlight):hover {
+ background-color: var(--color-gray);
+}
+
+.g-Table__Row--Header {
+ font-weight: bold;
+}
+
+.g-Table__Cell {
+ display: table-cell;
+ padding: var(--size-mouse) var(--size-dog);
+ white-space: nowrap;
+}
+
+.g-Table__Cell:first-child {
+ padding-left: 0;
+}
+
+.g-Table__Cell:last-child {
+ padding-right: 0;
+}
+
+.g-Table__NumericCell {
+ text-align: right;
+}
+
+/* Paging */
+
+.g-Paging {
+ display: flex;
+ justify-content: center;
+}
+
+.g-Paging__Link {
+ padding: 0 var(--size-dog);
+}
+
+.g-Paging__Link--Active {
+ color: var(--color-blue);
+}
+
+/* Form */
+
+.g-Form {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: var(--size-lion);
+}
+
+@media screen and (min-width: 500px) {
+ .g-Form {
+ width: 450px;
+ }
+}
+
+
+.g-Form__Label {
+ margin-bottom: var(--size-mouse);
+}
+
+.g-Form__Input {
+ margin-bottom: var(--size-lion);
+ background-color: white;
+ border: 1px solid black;
+ height: var(--size-camel);
+ padding: 0 var(--size-cat);
+ width: 100%;
+}
+
+.g-Form__InputColor {
+ padding: 0;
+}
+
+.g-Form__Select {
+ margin-bottom: var(--size-lion);
+ background-color: white;
+ border: 1px solid black;
+ height: var(--size-camel);
+ padding: 0 var(--size-cat);
+ width: 100%;
+}
+
+.g-Form__Error {
+ color: var(--color-red);
+ text-align: center;
+ margin-bottom: var(--size-bear);
+}
+
+/* Buttons */
+
+.g-Button__Search {
+ cursor: pointer;
+ background-color: var(--color-gray);
+ color: white;
+ border: none;
+ height: var(--size-camel);
+ padding: 0 var(--size-dog);
+ border-top: 1px solid black;
+ border-right: 1px solid black;
+ border-bottom: 1px solid black;
+}
+
+.g-Button__Validate {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ background-color: var(--color-green);
+ color: white;
+ border: none;
+ height: var(--size-camel);
+ padding: 0 var(--size-dog);
+ border-radius: var(--size-bee);
+}
+
+.g-Button__Validate:hover {
+ text-decoration: underline;
+}
+
+.g-Button__Danger {
+ cursor: pointer;
+ background-color: var(--color-red);
+ color: white;
+ border: none;
+ height: var(--size-camel);
+ padding: 0 var(--size-dog);
+ border-radius: var(--size-bee);
+}
+
+.g-Button__Danger:hover {
+ text-decoration: underline;
+}
+
+.g-Button__Danger:disabled {
+ background-color: var(--color-gray);
+ cursor: pointer;
+}
+
+/* Login */
+
+.g-Login {
+ margin-top: var(--size-elephant);
+}
+
+.g-Login__Button {
+ width: 100%;
+}
+
+/* Payment */
+
+.g-Payments__NoResults {
+ margin-top: var(--size-camel);
+ text-align: center;
+}
+
+.g-Payments__Header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+@media screen and (max-width: 500px) {
+ .g-Payments__Header {
+ flex-direction: column;
+ }
+}
+
+.g-Payments__FrequenciesAndSearch {
+ display: flex;
+ align-items: center;
+}
+
+.g-Payments__Search {
+ display: flex;
+ margin: 0 var(--size-bear) 0 0;
+}
+
+.g-Payments__SearchInput {
+ margin-bottom: 0;
+ width: 150px;
+}
+
+.g-Payments__Frequencies {
+ display: flex;
+ margin: 0 var(--size-dog);
+}
+
+.g-Payments__Frequency {
+ margin: 0 var(--size-mouse);
+}
+
+.g-Payments__Frequency--Selected {
+ color: var(--color-red);
+ font-weight: bold;
+}
+
+@media screen and (max-width: 500px) {
+ .g-Payments__New {
+ margin-top: var(--size-dog);
+ }
+}
+
+.g-Payments__Refund {
+ color: var(--color-green);
+}
+
+/* Balance */
+
+.g-Balance__ExceedingPayers {
+ background-color: var(--color-green);
+ border-radius: var(--size-bee);
+ color: white;
+ margin-bottom: var(--size-bear);
+ text-align: center;
+}
+
+.g-Balance__ExceedingPayer {
+ padding: var(--size-mouse);
+}
+
+/* Chart */
+
+.g-Chart {
+ width: 80%;
+ margin: 0 auto;
+}
diff --git a/assets/main.js b/assets/main.js
new file mode 100644
index 0000000..ba82a9f
--- /dev/null
+++ b/assets/main.js
@@ -0,0 +1,226 @@
+const path = window.location.pathname
+
+// Setting up interactivity according to the current page
+
+if (path == '/login') {
+
+ trim_inputs_on_blur()
+
+} else if (path == '/payment') { // Payment creation
+
+ trim_inputs_on_blur()
+ auto_fill_category()
+
+} else if (path.startsWith('/payment/')) { // Payment modification
+
+ trim_inputs_on_blur()
+ auto_fill_category()
+ control_remove_button()
+
+} else if (path == '/income') { // Income creation
+
+ trim_inputs_on_blur()
+
+} else if (path.startsWith('/income/')) { // Income modification
+
+ trim_inputs_on_blur()
+ control_remove_button()
+
+} else if (path == '/category') { // Category creation
+
+ trim_inputs_on_blur()
+
+} else if (path.startsWith('/category/')) { // Category modification
+
+ trim_inputs_on_blur()
+ control_remove_button()
+
+} else if (path == '/balance') {
+
+} else if (path == '/statistics') {
+
+ show_statistics()
+
+}
+
+// Functions
+
+function trim_inputs_on_blur() {
+ document.querySelectorAll('input').forEach(function (input) {
+ input.addEventListener('blur', function () {
+ input.value = input.value.trim()
+ })
+ })
+}
+
+function control_remove_button() {
+ const removeInput = document.getElementById('remove-input')
+ const removeButton = document.getElementById('remove-button')
+
+ if (removeInput && removeButton) {
+ removeInput.addEventListener('input', function() {
+ if (removeInput.value.trim() == removeInput.getAttribute('data-name')) {
+ removeButton.removeAttribute('disabled')
+ } else {
+ removeButton.setAttribute('disabled', true)
+ }
+ })
+ }
+}
+
+function auto_fill_category() {
+ const name = document.getElementById('name')
+ const category = document.getElementById('category_id')
+
+ function onNameChange() {
+ const query = name.value.trim()
+ if (query) {
+ const xhttp = new XMLHttpRequest()
+ xhttp.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ category.value = this.responseText
+ }
+ }
+ xhttp.open('GET', `/payment/category?payment_name=${query}`, true)
+ xhttp.send()
+ }
+ }
+
+ name.addEventListener('input', debounce(onNameChange, 500))
+}
+
+function show_statistics() {
+ const categories = JSON.parse(document.getElementById('categories').textContent)
+ const incomes = JSON.parse(document.getElementById('incomes').textContent)
+ const payments = JSON.parse(document.getElementById('payments').textContent)
+
+ const dates = incomes.map(function (i) { return i.date })
+
+ const datasets = [
+ {
+ label: 'Revenus',
+ data: incomes.map(function (i) { return i.amount }),
+ fill: false,
+ backgroundColor: '#222222',
+ borderColor: '#222222'
+ }
+ ]
+
+ const total_payments = {};
+ const categories_payments = {}
+ payments.forEach(function (p) {
+ if (categories_payments[p.category_id] === undefined) {
+ categories_payments[p.category_id] = {}
+ }
+ categories_payments[p.category_id][p.start_date] = p.cost
+
+ if (total_payments[p.start_date] === undefined) {
+ total_payments[p.start_date] = 0
+ }
+ total_payments[p.start_date] += p.cost
+ })
+
+ datasets.push({
+ label: 'Total des paiements',
+ data: dates.map(function (d) { return total_payments[d] || 0 }),
+ fill: false,
+ backgroundColor: '#555555',
+ borderColor: '#555555'
+ })
+
+ Object.keys(categories_payments).forEach(function (category_id) {
+ const category_payments = categories_payments[category_id]
+ const category = categories.find(function (c) { return c.id == category_id })
+ datasets.push({
+ label: category.name,
+ data: dates.map(function (d) { return category_payments[d] || 0 }),
+ fill: false,
+ backgroundColor: category.color,
+ borderColor: category.color,
+ })
+ })
+
+ const chart = new Chart(document.getElementById('g-Chart__Canvas').getContext('2d'), {
+ type: 'line',
+
+ data: {
+ labels: dates,
+ datasets
+ },
+
+ options: {
+ responsive: true,
+ tooltips: {
+ mode: 'nearest',
+ intersect: false,
+ callbacks: {
+ title: function(tooltipItem, data) {
+ return capitalize(prettyPrintMonth(tooltipItem[0].xLabel))
+ },
+ label: function(tooltipItem, data) {
+ let label = data.datasets[tooltipItem.datasetIndex].label || ''
+ if (label) {
+ label += ': '
+ }
+ label += `${tooltipItem.yLabel} €`
+ return label
+ }
+ }
+ },
+ hover: {
+ mode: 'nearest',
+ intersect: true
+ },
+ scales: {
+ xAxes: [
+ {
+ ticks: {
+ callback: prettyPrintMonth
+ }
+ }
+ ],
+ yAxes: [
+ {
+ ticks: {
+ beginAtZero: true,
+ callback: function(value) {
+ return `${value} €`
+ }
+ }
+ }
+ ]
+ }
+ }
+ })
+}
+
+function debounce(callback, delay) {
+ let timeout
+ return function() {
+ clearTimeout(timeout)
+ timeout = setTimeout(callback, delay)
+ }
+}
+
+function prettyPrintMonth(isoDate) {
+ xs = isoDate.split('-')
+ months = [
+ 'janvier',
+ 'février',
+ 'mars',
+ 'avril',
+ 'mai',
+ 'juin',
+ 'juillet',
+ 'août',
+ 'septembre',
+ 'octobre',
+ 'novembre',
+ 'décembre'
+ ]
+ return `${months[parseInt(xs[1]) - 1]} ${xs[0]}`
+}
+
+function capitalize(str) {
+ return str.replace(/^\w/, function (c) { return c.toUpperCase() })
+}
diff --git a/bin/db b/bin/db
new file mode 100755
index 0000000..431ba78
--- /dev/null
+++ b/bin/db
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd $(dirname "$0")/..
+
+DB_PATH="database.db"
+
+if [ "$1" == "init" ]; then
+
+ if [ -f "$DB_PATH" ]; then
+ rm "$DB_PATH"
+ fi
+
+ for MIGRATION in $(ls sql/migrations); do
+ printf "\n- Applying sql/migrations/$MIGRATION\n\n"
+ sqlite3 database.db < "sql/migrations/$MIGRATION"
+ done
+
+ printf "\n- Applying sql/fixtures.sql\n\n"
+ sqlite3 database.db < sql/fixtures.sql
+
+else
+
+ echo "Usage: $0 init"
+ exit 1
+
+fi
diff --git a/bin/watch b/bin/watch
new file mode 100755
index 0000000..84363fb
--- /dev/null
+++ b/bin/watch
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd $(dirname "$0")/..
+
+CMD=${1:-check}
+
+RUST_LOG=budget=info cargo-watch \
+ --ignore README.md \
+ --ignore bin \
+ --ignore sql \
+ --ignore database.db \
+ --ignore database.db-shm \
+ --ignore database.db-wal \
+ --clear \
+ -x "$CMD"
diff --git a/cabal-client.project b/cabal-client.project
deleted file mode 100644
index 182ead2..0000000
--- a/cabal-client.project
+++ /dev/null
@@ -1,4 +0,0 @@
-compiler: ghcjs
-packages:
- common/
- client/
diff --git a/cabal-server.project b/cabal-server.project
deleted file mode 100644
index 0ce5568..0000000
--- a/cabal-server.project
+++ /dev/null
@@ -1,3 +0,0 @@
-packages:
- common/
- server/
diff --git a/client/LICENSE b/client/LICENSE
deleted file mode 100644
index 45644ff..0000000
--- a/client/LICENSE
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- <one line to give the program's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- <program> Copyright (C) <year> <name of author>
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<http://www.gnu.org/licenses/>.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/client/Setup.hs b/client/Setup.hs
deleted file mode 100644
index 4467109..0000000
--- a/client/Setup.hs
+++ /dev/null
@@ -1,2 +0,0 @@
-import Distribution.Simple
-main = defaultMain
diff --git a/client/client.cabal b/client/client.cabal
deleted file mode 100644
index cf2c5a1..0000000
--- a/client/client.cabal
+++ /dev/null
@@ -1,90 +0,0 @@
-Name: client
-Version: 0.0.1
-License: GPL-3
-License-file: LICENSE
-Author: Joris Guyonvarch
-Maintainer: joris@guyonvarch.me
-Category: Web
-Build-type: Simple
-Cabal-version: >=1.10
-
-Executable client
- Main-Is: Main.hs
- -- Ghc-options: -Wall -Werror
- Hs-source-dirs: src
- Default-language: Haskell2010
-
- Default-extensions:
- ExistentialQuantification
- LambdaCase
- MultiParamTypeClasses
- OverloadedStrings
- RecursiveDo
- ScopedTypeVariables
-
- Build-depends:
- aeson
- , base >= 4.11 && < 5
- , bytestring
- , common
- , containers
- , data-default
- , ghcjs-dom-jsffi
- , jsaddle-dom
- , reflex-dom
- , text
- , time
- , validation
-
- -- Router
- , ghcjs-base
- , ghcjs-prim
- , ghcjs-dom
- , jsaddle
- , lens
- , uri-bytestring
-
- other-modules:
- Component.Appearing
- Component.Button
- Component.ConfirmDialog
- Component.Form
- Component.Input
- Component.Link
- Component.Modal
- Component.ModalForm
- Component.Pages
- Component.Select
- Component.Table
- Component.Tag
- Loadable
- Model.Route
- Util.Ajax
- Util.Css
- Util.Either
- Util.Reflex
- Util.Router
- Util.Validation
- Util.WaitFor
- View.App
- View.Header
- View.Icon
- View.Income.Form
- View.Income.Header
- View.Income.Income
- View.Income.Reducer
- View.Income.Table
- View.Category.Form
- View.Category.Category
- View.Category.Reducer
- View.Category.Table
- View.NotFound
- View.Payment.Form
- View.Payment.HeaderForm
- View.Payment.HeaderInfos
- View.Payment.Payment
- View.Payment.Reducer
- View.Payment.Table
- View.SignIn
- View.Statistics.Chart
- View.Statistics.Statistics
diff --git a/client/src/Component/Appearing.hs b/client/src/Component/Appearing.hs
deleted file mode 100644
index e0144ca..0000000
--- a/client/src/Component/Appearing.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-module Component.Appearing
- ( view
- ) where
-
-import Reflex.Dom (MonadWidget)
-import qualified Reflex.Dom as R
-
-view :: forall t m a. MonadWidget t m => m a -> m a
-view =
- R.divClass "g-Appearing"
diff --git a/client/src/Component/Button.hs b/client/src/Component/Button.hs
deleted file mode 100644
index 153a61b..0000000
--- a/client/src/Component/Button.hs
+++ /dev/null
@@ -1,57 +0,0 @@
-module Component.Button
- ( In(..)
- , Out(..)
- , view
- , defaultIn
- ) where
-
-import qualified Data.Map as M
-import Data.Maybe (catMaybes)
-import Data.Text (Text)
-import qualified Data.Text as T
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import qualified View.Icon as Icon
-
-data In t m = In
- { _in_class :: Dynamic t Text
- , _in_content :: m ()
- , _in_waiting :: Event t Bool
- , _in_tabIndex :: Maybe Int
- , _in_submit :: Bool
- }
-
-defaultIn :: forall t m. MonadWidget t m => m () -> In t m
-defaultIn content = In
- { _in_class = R.constDyn ""
- , _in_content = content
- , _in_waiting = R.never
- , _in_tabIndex = Nothing
- , _in_submit = False
- }
-
-data Out t = Out
- { _out_clic :: Event t ()
- }
-
-view :: forall t m. MonadWidget t m => In t m -> m (Out t)
-view input = do
- dynWaiting <- R.holdDyn False $ _in_waiting input
-
- let attr = do
- buttonClass <- _in_class input
- waiting <- dynWaiting
- return . M.fromList . catMaybes $
- [ Just ("type", if _in_submit input then "submit" else "button")
- , (\i -> ("tabindex", T.pack . show $ i)) <$> _in_tabIndex input
- , Just ("class", T.intercalate " " [ buttonClass, if waiting then "waiting" else "" ])
- ]
-
- (e, _) <- R.elDynAttr' "button" attr $ do
- Icon.loading
- R.divClass "content" $ _in_content input
-
- return $ Out
- { _out_clic = R.domEvent R.Click e
- }
diff --git a/client/src/Component/ConfirmDialog.hs b/client/src/Component/ConfirmDialog.hs
deleted file mode 100644
index cf26593..0000000
--- a/client/src/Component/ConfirmDialog.hs
+++ /dev/null
@@ -1,49 +0,0 @@
-module Component.ConfirmDialog
- ( In(..)
- , view
- ) where
-
-import Data.Text (Text)
-import Reflex.Dom (Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import qualified Common.Msg as Msg
-import qualified Component.Button as Button
-import qualified Component.Modal as Modal
-import qualified Util.Either as EitherUtil
-import qualified Util.WaitFor as WaitFor
-
-data In t m = In
- { _in_header :: Text
- , _in_confirm :: Event t () -> m (Event t ())
- }
-
-view :: forall t m a. MonadWidget t m => (In t m) -> Modal.Content t m
-view input _ =
- R.divClass "confirm" $ do
- R.divClass "confirmHeader" $
- R.text $ _in_header input
-
- R.divClass "confirmContent" $ do
- (confirm, cancel) <- R.divClass "buttons" $ do
-
- cancel <- Button._out_clic <$> (Button.view $
- (Button.defaultIn (R.text $ Msg.get Msg.Dialog_Undo))
- { Button._in_class = R.constDyn "undo" })
-
- rec
- confirm <- Button._out_clic <$> (Button.view $
- (Button.defaultIn (R.text $ Msg.get Msg.Dialog_Confirm))
- { Button._in_class = R.constDyn "confirm"
- , Button._in_submit = True
- , Button._in_waiting = waiting
- })
-
- (result, waiting) <- WaitFor.waitFor (_in_confirm input) confirm
-
- return (result, cancel)
-
- return $
- ( R.leftmost [ cancel, () <$ confirm ]
- , confirm
- )
diff --git a/client/src/Component/Form.hs b/client/src/Component/Form.hs
deleted file mode 100644
index 6878e68..0000000
--- a/client/src/Component/Form.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-module Component.Form
- ( view
- ) where
-
-import qualified Data.Map as M
-import Reflex.Dom (MonadWidget)
-import qualified Reflex.Dom as R
-
-view :: forall t m a. MonadWidget t m => m a -> m a
-view content =
- R.elAttr "form" (M.singleton "onsubmit" "event.preventDefault()") $
- content
diff --git a/client/src/Component/Input.hs b/client/src/Component/Input.hs
deleted file mode 100644
index bcff377..0000000
--- a/client/src/Component/Input.hs
+++ /dev/null
@@ -1,151 +0,0 @@
-module Component.Input
- ( In(..)
- , Out(..)
- , view
- , defaultIn
- ) where
-
-import qualified Data.Map as M
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time (NominalDiffTime)
-import Data.Validation (Validation (Failure, Success))
-import qualified Data.Validation as V
-import Reflex.Dom (Dynamic, Event, MonadWidget, Reflex,
- (&), (.~))
-import qualified Reflex.Dom as R
-
-import qualified Common.Util.Validation as ValidationUtil
-import qualified Component.Button as Button
-import qualified View.Icon as Icon
-
-data In a = In
- { _in_hasResetButton :: Bool
- , _in_label :: Text
- , _in_initialValue :: Text
- , _in_inputType :: Text
- , _in_validation :: Text -> Validation Text a
- }
-
-defaultIn :: In Text
-defaultIn = In
- { _in_hasResetButton = True
- , _in_label = ""
- , _in_initialValue = ""
- , _in_inputType = "text"
- , _in_validation = V.Success
- }
-
-data Out t a = Out
- { _out_raw :: Dynamic t Text
- , _out_value :: Dynamic t (Validation Text a)
- , _out_enter :: Event t ()
- }
-
-view
- :: forall t m a b. MonadWidget t m
- => In a
- -> Event t Text -- reset
- -> Event t b -- validate
- -> m (Out t a)
-view input reset validate = do
- rec
- let resetValue = R.leftmost
- [ reset
- , fmap (const "") resetClic
- ]
-
- inputAttr = R.ffor value (\v ->
- if T.null v && _in_inputType input /= "date" && _in_inputType input /= "color"
- then M.empty
- else M.singleton "class" "filled")
-
- value = R._textInput_value textInput
-
- containerAttr = R.ffor inputError (\e ->
- M.singleton "class" $ T.intercalate " "
- [ "textInput"
- , if Maybe.isJust e then "error" else ""
- ])
-
- let valueWithValidation = R.ffor value (\v -> (v, _in_validation input $ v))
- inputError <- getInputError valueWithValidation validate
-
- (textInput, resetClic) <- R.elDynAttr "div" containerAttr $ do
-
- textInput <- R.el "label" $ do
- textInput <- R.textInput $ R.def
- & R.attributes .~ inputAttr
- & R.setValue .~ resetValue
- & R.textInputConfig_initialValue .~ (_in_initialValue input)
- & R.textInputConfig_inputType .~ (_in_inputType input)
-
- R.divClass "label" $
- R.text (_in_label input)
-
- return textInput
-
- resetClic <-
- if _in_hasResetButton input
- then
- Button._out_clic <$> (Button.view $
- (Button.defaultIn Icon.cross)
- { Button._in_class = R.constDyn "reset"
- , Button._in_tabIndex = Just (-1)
- })
- else
- return R.never
-
- R.divClass "errorMessage" $
- R.dynText . fmap (Maybe.fromMaybe "") $ inputError
-
- return (textInput, resetClic)
-
- let enter = fmap (const ()) $ R.ffilter ((==) 13) . R._textInput_keypress $ textInput
-
- return $ Out
- { _out_raw = value
- , _out_value = fmap snd valueWithValidation
- , _out_enter = enter
- }
-
-getInputError
- :: forall t m a b c. MonadWidget t m
- => Dynamic t (Text, Validation Text a)
- -> Event t c
- -> m (Dynamic t (Maybe Text))
-getInputError validatedValue validate = do
- let errorDynamic = fmap (\(t, v) -> (t, validationError v)) validatedValue
- errorEvent = R.updated errorDynamic
- delayedError <- R.debounce (1 :: NominalDiffTime) errorEvent
- fmap (fmap fst) $ R.foldDyn
- (\event (err, hasBeenResetted) ->
- case event of
- ModifiedEvent t ->
- (Nothing, T.null t)
-
- ValidateEvent e ->
- (e, False)
-
- DelayEvent e ->
- if hasBeenResetted then
- (Nothing, False)
- else
- (e, False)
- )
- (Nothing, False)
- (R.leftmost
- [ fmap (\(t, _) -> ModifiedEvent t) errorEvent
- , fmap (\(_, e) -> DelayEvent e) delayedError
- , R.attachWith (\(_, e) _ -> ValidateEvent e) (R.current errorDynamic) validate
- ])
-
-validationError :: (Validation Text a) -> Maybe Text
-validationError (Failure e) = Just e
-validationError _ = Nothing
-
-data InputEvent
- = ModifiedEvent Text
- | DelayEvent (Maybe Text)
- | ValidateEvent (Maybe Text)
diff --git a/client/src/Component/Link.hs b/client/src/Component/Link.hs
deleted file mode 100644
index 1fd620e..0000000
--- a/client/src/Component/Link.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Component.Link
- ( view
- ) where
-
-import Data.Map (Map)
-import qualified Data.Map as M
-import Data.Text (Text)
-import qualified Data.Text as T
-import Reflex.Dom (Dynamic, MonadWidget)
-import qualified Reflex.Dom as R
-
-view :: forall t m a. MonadWidget t m => Text -> Dynamic t (Map Text Text) -> Text -> m ()
-view href inputAttrs content =
- R.elDynAttr "a" attrs (R.text content)
- where
-
- onclickHandler =
- T.intercalate ";"
- [ "history.pushState(0, '', event.target.href)"
- , "dispatchEvent(new PopStateEvent('popstate', {cancelable: true, bubbles: true, view: window}))"
- , "return false"
- ]
-
- attrs =
- R.ffor inputAttrs (\as ->
- (M.union
- (M.fromList
- [ ("onclick", onclickHandler)
- , ("href", href)
- ]
- )
- as)
- )
diff --git a/client/src/Component/Modal.hs b/client/src/Component/Modal.hs
deleted file mode 100644
index 46d3f64..0000000
--- a/client/src/Component/Modal.hs
+++ /dev/null
@@ -1,117 +0,0 @@
-module Component.Modal
- ( In(..)
- , Content
- , view
- ) where
-
-import Control.Monad (void)
-import qualified Data.Map as M
-import qualified Data.Map.Lazy as LM
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time.Clock (NominalDiffTime)
-import qualified GHCJS.DOM.Element as Element
-import qualified GHCJS.DOM.Node as Node
-import JSDOM.Types (JSString)
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-import qualified Reflex.Dom.Class as R
-
-import qualified Util.Reflex as ReflexUtil
-
--- Content = CurtainClickEvent -> (CancelEvent, ConfirmEvent)
-type Content t m = Event t () -> m (Event t (), Event t ())
-
-data In t m = In
- { _in_show :: Event t ()
- , _in_content :: Content t m
- }
-
-view :: forall t m a. MonadWidget t m => In t m -> m (Event t ())
-view input = do
- rec
- let show = Show <$ (_in_show input)
-
- startHiding =
- R.attachWithMaybe
- (\a _ -> if a then Just StartHiding else Nothing)
- (R.current canBeHidden)
- (R.leftmost [ hide, curtainClick ])
-
- canBeHidden <-
- R.holdDyn True $ R.leftmost
- [ False <$ startHiding
- , True <$ endHiding
- ]
-
- endHiding <-
- R.delay (0.2 :: NominalDiffTime) (EndHiding <$ startHiding)
-
- let action =
- R.leftmost [ show, startHiding, endHiding ]
-
- modalClass <-
- R.holdDyn "" (fmap getModalClass action)
-
- (elem, dyn) <-
- R.buildElement "div" (getAttributes <$> modalClass) $
- ReflexUtil.visibleIfEvent
- (isVisible <$> action)
- (R.blank >> return (R.never, R.never, R.never))
- (do
- (curtain, _) <- R.elAttr' "div" (M.singleton "class" "g-Modal__Curtain") $ R.blank
- let curtainClick = R.domEvent R.Click curtain
- (hide, content) <- R.divClass "g-Modal__Content" (_in_content input curtainClick)
- return (curtainClick, hide, content))
-
-
- performShowEffects action elem
-
- let curtainClick = R.switchDyn $ (\(a, _, _) -> a) <$> dyn
- let hide = R.switchDyn $ (\(_, b, _) -> b) <$> dyn
- let content = R.switchDyn $ (\(_, _, c) -> c) <$> dyn
-
- -- Delay the event in order to let time for the modal to disappear
- R.delay (0.5 :: NominalDiffTime) content
-
-getAttributes :: Text -> LM.Map Text Text
-getAttributes modalClass =
- M.singleton "class" $
- T.intercalate " " [ "g-Modal", modalClass]
-
-performShowEffects
- :: forall t m a. MonadWidget t m
- => Event t Action
- -> Element.Element
- -> m ()
-performShowEffects showEvent elem = do
- body <- ReflexUtil.getBody
-
- let showEffects =
- flip fmap showEvent (\case
- Show -> do
- Node.appendChild body elem
- Element.setClassName body ("g-Body--Modal" :: JSString)
- StartHiding ->
- return ()
- EndHiding -> do
- Node.removeChild body elem
- Element.setClassName body ("" :: JSString)
- )
-
- R.performEvent_ $ void `fmap` showEffects
-
-data Action
- = Show
- | StartHiding
- | EndHiding
-
-getModalClass :: Action -> Text
-getModalClass Show = "g-Modal--Show"
-getModalClass StartHiding = "g-Modal--Hiding"
-getModalClass _ = ""
-
-isVisible :: Action -> Bool
-isVisible Show = True
-isVisible StartHiding = True
-isVisible EndHiding = False
diff --git a/client/src/Component/ModalForm.hs b/client/src/Component/ModalForm.hs
deleted file mode 100644
index c56ff88..0000000
--- a/client/src/Component/ModalForm.hs
+++ /dev/null
@@ -1,71 +0,0 @@
-module Component.ModalForm
- ( view
- , In(..)
- , Out(..)
- ) where
-
-import Data.Aeson (ToJSON)
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time.Calendar (Day)
-import Data.Validation (Validation)
-import qualified Data.Validation as V
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import qualified Common.Msg as Msg
-import qualified Component.Button as Button
-import qualified Component.Form as Form
-import qualified Util.Either as EitherUtil
-import qualified Util.Validation as ValidationUtil
-import qualified Util.WaitFor as WaitFor
-
-data In m t a e = In
- { _in_headerLabel :: Text
- , _in_form :: m (Dynamic t (Validation e a))
- , _in_ajax :: Event t a -> m (Event t (Either Text ()))
- }
-
-data Out t = Out
- { _out_hide :: Event t ()
- , _out_cancel :: Event t ()
- , _out_confirm :: Event t ()
- , _out_validate :: Event t ()
- }
-
-view :: forall t m a e. (MonadWidget t m, ToJSON a) => In m t a e -> m (Out t)
-view input =
- R.divClass "form" $ do
- R.divClass "formHeader" $
- R.text (_in_headerLabel input)
-
- Form.view $
- R.divClass "formContent" $ do
- rec
- form <- _in_form input
-
- (validate, cancel, confirm) <- R.divClass "buttons" $ do
- rec
- cancel <- Button._out_clic <$> (Button.view $
- (Button.defaultIn (R.text $ Msg.get Msg.Dialog_Undo))
- { Button._in_class = R.constDyn "undo" })
-
- confirm <- Button._out_clic <$> (Button.view $
- (Button.defaultIn (R.text $ Msg.get Msg.Dialog_Confirm))
- { Button._in_class = R.constDyn "confirm"
- , Button._in_waiting = waiting
- , Button._in_submit = True
- })
-
- (validate, waiting) <- WaitFor.waitFor
- (_in_ajax input)
- (ValidationUtil.fireValidation form confirm)
-
- return (R.fmapMaybe EitherUtil.eitherToMaybe validate, cancel, confirm)
-
- return Out
- { _out_hide = R.leftmost [ cancel, () <$ validate ]
- , _out_cancel = cancel
- , _out_confirm = confirm
- , _out_validate = validate
- }
diff --git a/client/src/Component/Pages.hs b/client/src/Component/Pages.hs
deleted file mode 100644
index d54cd3d..0000000
--- a/client/src/Component/Pages.hs
+++ /dev/null
@@ -1,86 +0,0 @@
-module Component.Pages
- ( view
- , In(..)
- , Out(..)
- ) where
-
-import qualified Data.Text as T
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import qualified Component.Button as Button
-
-import qualified Util.Reflex as ReflexUtil
-import qualified View.Icon as Icon
-
-data In t = In
- { _in_total :: Dynamic t Int
- , _in_perPage :: Int
- , _in_page :: Int
- }
-
-data Out t = Out
- { _out_newPage :: Event t Int
- }
-
-view :: forall t m. MonadWidget t m => In t -> m (Out t)
-view input = do
- newPage <- ReflexUtil.divVisibleIf ((> 0) <$> (_in_total input)) $ pageButtons input
-
- return $ Out
- { _out_newPage = newPage
- }
-
-pageButtons
- :: forall t m. MonadWidget t m
- => In t
- -> m (Event t Int)
-pageButtons input = do
- R.divClass "pages" $ do
- rec
- let newPage = R.leftmost
- [ firstPageClic
- , previousPageClic
- , pageClic
- , nextPageClic
- , lastPageClic
- ]
-
- currentPage <- R.holdDyn (_in_page input) newPage
-
- firstPageClic <- pageButton noCurrentPage (R.constDyn 1) Icon.doubleLeftBar
-
- previousPageClic <- pageButton noCurrentPage (fmap (\x -> max (x - 1) 1) currentPage) Icon.doubleLeft
-
- pageClic <- pageEvent <$> (R.simpleList (range <$> currentPage <*> maxPage) $ \p ->
- pageButton (Just <$> currentPage) p (R.dynText $ fmap (T.pack . show) p))
-
- nextPageClic <- pageButton noCurrentPage ((\c m -> min (c + 1) m) <$> currentPage <*> maxPage) Icon.doubleRight
-
- lastPageClic <- pageButton noCurrentPage maxPage Icon.doubleRightBar
-
- return newPage
-
- where maxPage = R.ffor (_in_total input) (\t -> ceiling $ toRational t / toRational (_in_perPage input))
- pageEvent = R.switch . R.current . fmap R.leftmost
- noCurrentPage = R.constDyn Nothing
-
-range :: Int -> Int -> [Int]
-range currentPage maxPage = [start..end]
- where sidePages = 2
- start = max 1 (min (currentPage - sidePages) (maxPage - sidePages * 2))
- end = min maxPage (start + sidePages * 2)
-
-pageButton :: forall t m. MonadWidget t m => Dynamic t (Maybe Int) -> Dynamic t Int -> m () -> m (Event t Int)
-pageButton currentPage page content = do
- clic <- Button._out_clic <$> (Button.view $ Button.In
- { Button._in_class = do
- cp <- currentPage
- p <- page
- if cp == Just p then "page current" else "page"
- , Button._in_content = content
- , Button._in_waiting = R.never
- , Button._in_tabIndex = Nothing
- , Button._in_submit = False
- })
- return . fmap fst $ R.attach (R.current page) clic
diff --git a/client/src/Component/Select.hs b/client/src/Component/Select.hs
deleted file mode 100644
index 70f5f58..0000000
--- a/client/src/Component/Select.hs
+++ /dev/null
@@ -1,80 +0,0 @@
-module Component.Select
- ( view
- , In(..)
- , Out(..)
- ) where
-
-import Data.Map (Map)
-import qualified Data.Map as M
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Validation (Validation)
-import Reflex.Dom (Dynamic, Event, MonadWidget, Reflex)
-import qualified Reflex.Dom as R
-
-import qualified Util.Validation as ValidationUtil
-
-data (Reflex t) => In t a b c = In
- { _in_label :: Text
- , _in_initialValue :: a
- , _in_value :: Event t a
- , _in_values :: Dynamic t (Map a Text)
- , _in_reset :: Event t b
- , _in_isValid :: a -> Validation Text a
- , _in_validate :: Event t c
- }
-
-data Out t a = Out
- { _out_raw :: Dynamic t a
- , _out_value :: Dynamic t (Validation Text a)
- }
-
-view :: forall t m a b c. (Ord a, MonadWidget t m) => In t a b c -> m (Out t a)
-view input = do
- rec
- let containerAttr = R.ffor showedError (\e ->
- M.singleton "class" $ T.intercalate " "
- [ "input selectInput"
- , if Maybe.isJust e then "error" else ""
- ])
-
- validatedValue =
- fmap (_in_isValid input) value
-
- maybeError =
- fmap ValidationUtil.maybeError validatedValue
-
- showedError <- R.holdDyn Nothing $ R.leftmost
- [ Nothing <$ _in_reset input
- , R.updated maybeError
- , R.attachWith const (R.current maybeError) (_in_validate input)
- ]
-
- value <- R.elDynAttr "div" containerAttr $ do
- let initialValue = _in_initialValue input
-
- let setValue = R.leftmost
- [ initialValue <$ (_in_reset input)
- , _in_value input
- ]
-
- value <- R.el "label" $ do
- R.divClass "label" $
- R.text (_in_label input)
-
- R._dropdown_value <$>
- R.dropdown
- initialValue
- (_in_values input)
- (R.def { R._dropdownConfig_setValue = setValue })
-
- R.divClass "errorMessage" . R.dynText $
- R.ffor showedError (Maybe.fromMaybe "")
-
- return value
-
- return Out
- { _out_raw = value
- , _out_value = validatedValue
- }
diff --git a/client/src/Component/Table.hs b/client/src/Component/Table.hs
deleted file mode 100644
index 1482f91..0000000
--- a/client/src/Component/Table.hs
+++ /dev/null
@@ -1,105 +0,0 @@
-module Component.Table
- ( view
- , In(..)
- , Out(..)
- ) where
-
-import qualified Data.Map as M
-import Data.Text (Text)
-import Reflex.Dom (Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import qualified Component.Button as Button
-import qualified Component.Modal as Modal
-import qualified Util.Reflex as ReflexUtil
-import qualified View.Icon as Icon
-
-data In m t h r = In
- { _in_headerLabel :: h -> Text
- , _in_rows :: [r]
- , _in_cell :: h -> r -> m ()
- , _in_cloneModal :: r -> Modal.Content t m
- , _in_editModal :: r -> Modal.Content t m
- , _in_deleteModal :: r -> Modal.Content t m
- , _in_canEdit :: r -> Bool
- , _in_canDelete :: r -> Bool
- }
-
-data Out t = Out
- { _out_add :: Event t ()
- , _out_edit :: Event t ()
- , _out_delete :: Event t ()
- }
-
-view :: forall t m h r. (MonadWidget t m, Bounded h, Enum h) => In m t h r -> m (Out t)
-view input =
- R.divClass "table" $ do
- rec
- result <- R.divClass "lines" $ do
-
- R.divClass "header" $ do
- flip mapM_ [minBound..] $ \header ->
- R.divClass "cell" . R.text $
- _in_headerLabel input header
-
- R.divClass "cell" $ R.blank
- R.divClass "cell" $ R.blank
- R.divClass "cell" $ R.blank
-
- flip mapM (_in_rows input) $ \row ->
- R.divClass "row" $ do
- flip mapM_ [minBound..] $ \header ->
- R.divClass "cell" $
- _in_cell input header row
-
- cloneButton <-
- R.divClass "cell button" $
- Button._out_clic <$> (Button.view $
- Button.defaultIn Icon.clone)
-
- clone <-
- Modal.view $ Modal.In
- { Modal._in_show = cloneButton
- , Modal._in_content = _in_cloneModal input row
- }
-
- let visibleIf cond =
- R.elAttr
- "div"
- (if cond then M.empty else M.singleton "style" "display:none")
-
- editButton <-
- R.divClass "cell button" $
- visibleIf (_in_canEdit input row) $
- Button._out_clic <$> (Button.view $
- Button.defaultIn Icon.edit)
-
- edit <-
- Modal.view $ Modal.In
- { Modal._in_show = editButton
- , Modal._in_content = _in_editModal input row
- }
-
- deleteButton <-
- R.divClass "cell button" $
- visibleIf (_in_canDelete input row) $
- Button._out_clic <$> (Button.view $
- Button.defaultIn Icon.delete)
-
- delete <-
- Modal.view $ Modal.In
- { Modal._in_show = deleteButton
- , Modal._in_content = _in_deleteModal input row
- }
-
- return (clone, edit, delete)
-
- let add = R.leftmost . map (\(a, _, _) -> a) $ result
- edit = R.leftmost . map (\(_, a, _) -> a) $ result
- delete = R.leftmost . map (\(_, _, a) -> a) $ result
-
- return $ Out
- { _out_add = add
- , _out_edit = edit
- , _out_delete = delete
- }
diff --git a/client/src/Component/Tag.hs b/client/src/Component/Tag.hs
deleted file mode 100644
index f75b8d3..0000000
--- a/client/src/Component/Tag.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Component.Tag
- ( In(..)
- , view
- ) where
-
-import qualified Data.Map as M
-import Data.Text (Text)
-import qualified Data.Text as T
-import Reflex.Dom (MonadWidget)
-import qualified Reflex.Dom as R
-
-data In = In
- { _in_text :: Text
- , _in_color :: Text
- }
-
-view :: forall t m a. MonadWidget t m => In -> m ()
-view input =
- R.elAttr "span" attrs $
- R.text $ _in_text input
-
- where
- attrs =
- M.fromList
- [ ("class", "tag")
- , ("style", T.concat [ "background-color: ", _in_color input ])
- ]
diff --git a/client/src/Loadable.hs b/client/src/Loadable.hs
deleted file mode 100644
index 4806b08..0000000
--- a/client/src/Loadable.hs
+++ /dev/null
@@ -1,109 +0,0 @@
-module Loadable
- ( Loadable (..)
- , fromEither
- , fromEvent
- , viewHideValueWhileLoading
- , viewShowValueWhileLoading
- ) where
-
-import qualified Data.Map as M
-import Reflex.Dom (MonadWidget)
-import qualified Reflex.Dom as R
-
-import Data.Functor (Functor)
-import Data.Text (Text)
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-data Loadable t
- = Loading
- | Error Text
- | Loaded t
- deriving (Eq, Show)
-
-instance Functor Loadable where
- fmap f Loading = Loading
- fmap f (Error e) = Error e
- fmap f (Loaded x) = Loaded (f x)
-
-instance Applicative Loadable where
- pure x = Loaded x
-
- Loading <*> _ = Loading
- (Error e) <*> _ = Error e
- (Loaded f) <*> Loading = Loading
- (Loaded f) <*> (Error e) = Error e
- (Loaded f) <*> (Loaded x) = Loaded (f x)
-
-instance Monad Loadable where
- Loading >>= f = Loading
- (Error e) >>= f = Error e
- (Loaded x) >>= f = f x
-
-fromEither :: forall a b. Either Text b -> Loadable b
-fromEither (Left err) = Error err
-fromEither (Right value) = Loaded value
-
-fromEvent :: forall t m a. MonadWidget t m => Event t (Either Text a) -> m (Dynamic t (Loadable a))
-fromEvent =
- R.foldDyn
- (\res _ -> case res of
- Left err -> Error err
- Right t -> Loaded t
- )
- Loading
-
-viewHideValueWhileLoading :: forall t m a b. MonadWidget t m => (a -> m b) -> Loadable a -> m (Maybe b)
-viewHideValueWhileLoading f loadable =
- case loadable of
- Loading ->
- (R.divClass "pageSpinner" $ R.divClass "spinner" $ R.blank) >> return Nothing
-
- Error err ->
- R.text err >> return Nothing
-
- Loaded x ->
- Just <$> f x
-
-viewShowValueWhileLoading
- :: forall t m a b. (MonadWidget t m, Eq a)
- => Dynamic t (Loadable a)
- -> (a -> m b)
- -> m (Event t (Maybe b))
-viewShowValueWhileLoading loadable f = do
-
- value <-
- (R.foldDyn
- (\l v1 ->
- case l of
- Loaded v2 -> Just v2
- _ -> v1)
- Nothing
- (R.updated loadable)) >>= R.holdUniqDyn
-
- withLoader (fmap ((==) Loading) loadable) $
- R.dyn . R.ffor value $ \case
- Nothing -> return Nothing
- Just x -> Just <$> f x
-
-withLoader
- :: forall t m a. MonadWidget t m
- => Dynamic t Bool
- -> m a
- -> m a
-withLoader isLoading block =
- R.divClass "g-Loadable" $ do
- res <- R.elDynAttr "div" (blockAttrs <$> isLoading) $
- block
- R.elDynAttr "div" (spinnerAttrs <$> isLoading) $
- R.divClass "spinner" R.blank
- return res
-
- where
- spinnerAttrs l = M.singleton "class" $
- "g-Loadable__Spinner"
- <> (if l then " g-Loadable__Spinner--Loading" else "")
-
- blockAttrs l = M.singleton "class" $
- "g-Loadable__Content"
- <> (if l then " g-Loadable__Content--Loading" else "")
diff --git a/client/src/Main.hs b/client/src/Main.hs
deleted file mode 100644
index c71b0f0..0000000
--- a/client/src/Main.hs
+++ /dev/null
@@ -1,39 +0,0 @@
-module Main
- ( main
- ) where
-
-import qualified Data.Aeson as Aeson
-import qualified Data.ByteString.Lazy as LB
-import qualified Data.JSString.Text as Dom
-import qualified Data.Text.Encoding as T
-import qualified JSDOM as Dom
-import qualified JSDOM.Generated.HTMLElement as Dom
-import qualified JSDOM.Generated.NonElementParentNode as Dom
-import JSDOM.Types (HTMLElement (..), JSM,
- JSString)
-import qualified JSDOM.Types as Dom
-import Prelude hiding (error, init)
-
-import Common.Model (Init)
-import qualified Common.Msg as Msg
-
-import qualified View.App as App
-
-main :: JSM ()
-main = do
- initResult <- readInit
- App.widget initResult
-
-readInit :: JSM (Maybe Init)
-readInit = do
- document <- Dom.currentDocumentUnchecked
- initNode <- Dom.getElementById document ("init" :: JSString)
-
- case initNode of
- Just node -> do
- text <- Dom.textFromJSString <$> Dom.getInnerText (Dom.uncheckedCastTo HTMLElement node)
- return $ case Aeson.decode (LB.fromStrict . T.encodeUtf8 $ text) of
- Just init -> init
- Nothing -> Nothing
- _ ->
- return Nothing
diff --git a/client/src/Model/Route.hs b/client/src/Model/Route.hs
deleted file mode 100644
index f92e9be..0000000
--- a/client/src/Model/Route.hs
+++ /dev/null
@@ -1,11 +0,0 @@
-module Model.Route
- ( Route(..)
- ) where
-
-data Route
- = RootRoute
- | IncomeRoute
- | CategoryRoute
- | StatisticsRoute
- | NotFoundRoute
- deriving (Eq, Show)
diff --git a/client/src/Util/Ajax.hs b/client/src/Util/Ajax.hs
deleted file mode 100644
index dcfd402..0000000
--- a/client/src/Util/Ajax.hs
+++ /dev/null
@@ -1,139 +0,0 @@
-module Util.Ajax
- ( getNow
- , get
- , post
- , postAndParseResult
- , put
- , putAndParseResult
- , delete
- ) where
-
-import Control.Arrow (left)
-import Data.Aeson (FromJSON, ToJSON)
-import qualified Data.Aeson as Aeson
-import Data.ByteString (ByteString)
-import qualified Data.ByteString.Lazy as LBS
-import Data.Default (def)
-import qualified Data.Map.Lazy as LM
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Text.Encoding as T
-import Data.Time.Clock (NominalDiffTime)
-import Reflex.Dom (Dynamic, Event, IsXhrPayload,
- MonadWidget, XhrRequest,
- XhrRequestConfig (..), XhrResponse,
- XhrResponseHeaders (..))
-import qualified Reflex.Dom as R
-
-import Loadable (Loadable)
-import qualified Loadable
-
-getNow :: forall t m a. (MonadWidget t m, FromJSON a) => Text -> m (Dynamic t (Loadable a))
-getNow url = do
- postBuild <- R.getPostBuild
- get (url <$ postBuild)
- >>= R.debounce (0 :: NominalDiffTime) -- Fired 2 times otherwise
- >>= Loadable.fromEvent
-
-get
- :: forall t m a. (MonadWidget t m, FromJSON a)
- => Event t Text
- -> m (Event t (Either Text a))
-get url =
- fmap getJsonResult <$>
- R.performRequestAsync (R.ffor url $ \u -> jsonRequest "GET" u (Aeson.String ""))
-
-post
- :: forall t m a. (MonadWidget t m, ToJSON a)
- => Text
- -> Event t a
- -> m (Event t (Either Text ()))
-post url input =
- fmap checkResult <$>
- R.performRequestAsync (jsonRequest "POST" url <$> input)
-
-postAndParseResult
- :: forall t m a b. (MonadWidget t m, ToJSON a, FromJSON b)
- => Text
- -> Event t a
- -> m (Event t (Either Text b))
-postAndParseResult url input =
- fmap getJsonResult <$>
- R.performRequestAsync (jsonRequest "POST" url <$> input)
-
-put
- :: forall t m a. (MonadWidget t m, ToJSON a)
- => Text
- -> Event t a
- -> m (Event t (Either Text ()))
-put url input =
- fmap checkResult <$>
- R.performRequestAsync (jsonRequest "PUT" url <$> input)
-
-putAndParseResult
- :: forall t m a b. (MonadWidget t m, ToJSON a, FromJSON b)
- => Text
- -> Event t a
- -> m (Event t (Either Text b))
-putAndParseResult url input =
- fmap getJsonResult <$>
- R.performRequestAsync (jsonRequest "PUT" url <$> input)
-
-delete
- :: forall t m a. (MonadWidget t m)
- => Dynamic t Text
- -> Event t ()
- -> m (Event t (Either Text Text))
-delete url fire = do
- fmap getResult <$>
- (R.performRequestAsync $
- R.attachWith (\u _ -> request "DELETE" u ()) (R.current url) fire)
-
-checkResult :: XhrResponse -> Either Text ()
-checkResult response =
- () <$ getResult response
-
-getJsonResult :: forall a. (FromJSON a) => XhrResponse -> Either Text a
-getJsonResult response =
- case getResult response of
- Left l -> Left l
- Right r -> left T.pack . Aeson.eitherDecodeStrict $ (T.encodeUtf8 r)
-
-getResult :: XhrResponse -> Either Text Text
-getResult response =
- case R._xhrResponse_responseText response of
- Just responseText ->
- if R._xhrResponse_status response == 200
- then Right responseText
- else Left responseText
- _ -> Left "NoKey"
-
-request :: forall a. (IsXhrPayload a) => Text -> Text -> a -> XhrRequest a
-request method url payload =
- let
- config = XhrRequestConfig
- { _xhrRequestConfig_headers = def
- , _xhrRequestConfig_user = def
- , _xhrRequestConfig_password = def
- , _xhrRequestConfig_responseType = def
- , _xhrRequestConfig_responseHeaders = def
- , _xhrRequestConfig_withCredentials = False
- , _xhrRequestConfig_sendData = payload
- }
- in
- R.xhrRequest method url config
-
-jsonRequest :: forall a. (ToJSON a) => Text -> Text -> a -> XhrRequest ByteString
-jsonRequest method url payload =
- let
- config = XhrRequestConfig
- { _xhrRequestConfig_headers = def
- , _xhrRequestConfig_user = def
- , _xhrRequestConfig_password = def
- , _xhrRequestConfig_responseType = def
- , _xhrRequestConfig_responseHeaders = def
- , _xhrRequestConfig_withCredentials = False
- , _xhrRequestConfig_sendData = LBS.toStrict $ Aeson.encode payload
- }
- in
- R.xhrRequest method url config
diff --git a/client/src/Util/Css.hs b/client/src/Util/Css.hs
deleted file mode 100644
index 804b10f..0000000
--- a/client/src/Util/Css.hs
+++ /dev/null
@@ -1,9 +0,0 @@
-module Util.Css
- ( classes
- ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-
-classes :: [(Text, Bool)] -> Text
-classes = T.unwords . map fst . filter snd
diff --git a/client/src/Util/Either.hs b/client/src/Util/Either.hs
deleted file mode 100644
index e76bc8a..0000000
--- a/client/src/Util/Either.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Util.Either
- ( eitherToMaybe
- ) where
-
-eitherToMaybe :: forall a b. Either a b -> Maybe b
-eitherToMaybe (Right b) = Just b
-eitherToMaybe _ = Nothing
diff --git a/client/src/Util/Reflex.hs b/client/src/Util/Reflex.hs
deleted file mode 100644
index aa5cebb..0000000
--- a/client/src/Util/Reflex.hs
+++ /dev/null
@@ -1,59 +0,0 @@
-module Util.Reflex
- ( visibleIfDyn
- , visibleIfEvent
- , divVisibleIf
- , divClassVisibleIf
- , flatten
- , flattenTuple
- , getBody
- ) where
-
-import qualified Data.Map as M
-import Data.Text (Text)
-import qualified GHCJS.DOM as Dom
-import qualified GHCJS.DOM.Document as Document
-import qualified GHCJS.DOM.HTMLCollection as HTMLCollection
-import GHCJS.DOM.Types (Element)
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-visibleIfDyn :: forall t m a. MonadWidget t m => Dynamic t Bool -> m a -> m a -> m (Event t a)
-visibleIfDyn cond empty content =
- R.dyn $ R.ffor cond $ \case
- True -> content
- False -> empty
-
-visibleIfEvent :: forall t m a. MonadWidget t m => Event t Bool -> m a -> m a -> m (Dynamic t a)
-visibleIfEvent cond empty content =
- R.widgetHold empty $
- R.ffor cond $ \case
- True -> content
- False -> empty
-
-divVisibleIf :: forall t m a. MonadWidget t m => Dynamic t Bool -> m a -> m a
-divVisibleIf cond content = divClassVisibleIf cond "" content
-
-divClassVisibleIf :: forall t m a. MonadWidget t m => Dynamic t Bool -> Text -> m a -> m a
-divClassVisibleIf cond className content =
- R.elDynAttr
- "div"
- (fmap (\c -> (M.singleton "class" className) `M.union` if c then M.empty else M.singleton "style" "display:none") cond)
- content
-
-flatten :: forall t m a. MonadWidget t m => Event t (Event t a) -> m (Event t a)
-flatten e = do
- dyn <- R.holdDyn R.never e
- return $ R.switchDyn dyn
-
-flattenTuple
- :: forall t m a b. MonadWidget t m
- => Event t (Event t a, Event t b)
- -> m (Event t a, Event t b)
-flattenTuple e = (,) <$> (flatten $ fmap fst e) <*> (flatten $ fmap snd e)
-
-getBody :: forall t m. MonadWidget t m => m Element
-getBody = do
- document <- Dom.currentDocumentUnchecked
- nodelist <- Document.getElementsByTagName document ("body" :: String)
- Just body <- nodelist `HTMLCollection.item` 0
- return body
diff --git a/client/src/Util/Router.hs b/client/src/Util/Router.hs
deleted file mode 100644
index e9d0a1a..0000000
--- a/client/src/Util/Router.hs
+++ /dev/null
@@ -1,266 +0,0 @@
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE ConstraintKinds #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE ForeignFunctionInterface #-}
-{-# LANGUAGE JavaScriptFFI #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE RecursiveDo #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TemplateHaskell #-}
-{-# LANGUAGE TypeFamilies #-}
-
-module Util.Router (
- -- == High-level routers
- route
- , route'
- , partialPathRoute
-
- -- = Low-level URL bar access
- , getLoc
- , getURI
- , getUrlText
- , uriOrigin
- , URI
-
- -- = History movement
- , goForward
- , goBack
- ) where
-
-------------------------------------------------------------------------------
-import Control.Lens ((&), (.~), (^.))
-import Control.Monad.Fix (MonadFix)
-import qualified Data.ByteString.Char8 as BS
-import Data.Monoid ((<>))
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Text.Encoding as T
-import GHCJS.DOM (currentDocumentUnchecked,
- currentWindowUnchecked)
-import GHCJS.DOM.Document (createEvent)
-import GHCJS.DOM.Event (initEvent)
-import GHCJS.DOM.EventM (on)
-import GHCJS.DOM.EventTarget (dispatchEvent_)
-import GHCJS.DOM.History (History, back, forward,
- pushState)
-import GHCJS.DOM.Location (getHref)
-import GHCJS.DOM.PopStateEvent
-import GHCJS.DOM.Types (Location (..),
- PopStateEvent (..))
-import GHCJS.DOM.Types (MonadJSM, uncheckedCastTo)
-import qualified GHCJS.DOM.Types as DOM
-import GHCJS.DOM.Window (getHistory, getLocation)
-import GHCJS.DOM.WindowEventHandlers (popState)
-import GHCJS.Foreign (isFunction)
-import GHCJS.Marshal.Pure (pFromJSVal)
-import Language.Javascript.JSaddle (JSM, Object (..), ghcjsPure,
- liftJSM)
-import qualified Language.Javascript.JSaddle as JS
-import Reflex.Dom.Core hiding (EventName, Window)
-import qualified URI.ByteString as U
-------------------------------------------------------------------------------
-
-
--------------------------------------------------------------------------------
--- | Manipulate and track the URL 'GHCJS.DOM.Types.Location' for dynamic
--- routing of a widget
--- These sources of URL-bar change will be reflected in the output URI
--- - Input events to 'route'
--- - Browser Forward/Back button clicks
--- - forward/back javascript calls (or 'goForward'/'goBack') Haskell calls
--- - Any URL changes followed by a popState event
--- But external calls to pushState that don't manually fire a popState
--- won't be detected
-route
- :: forall t m.
- ( MonadHold t m
- , PostBuild t m
- , TriggerEvent t m
- , PerformEvent t m
- , HasJSContext m
- , HasJSContext (Performable m)
- , MonadJSM m
- , MonadJSM (Performable m))
- => Event t T.Text
- -> m (Dynamic t (U.URIRef U.Absolute))
-route pushTo = do
- loc0 <- getURI
-
- _ <- performEvent $ ffor pushTo $ \t -> do
- let newState = Just t
- withHistory $ \h -> pushState h (0 :: Double) ("" :: T.Text) (newState :: Maybe T.Text)
- liftJSM dispatchEvent'
-
- locUpdates <- getPopState
- holdDyn loc0 locUpdates
-
-route'
- :: forall t m a b.
- ( MonadHold t m
- , PostBuild t m
- , TriggerEvent t m
- , PerformEvent t m
- , HasJSContext m
- , HasJSContext (Performable m)
- , MonadJSM m
- , MonadJSM (Performable m)
- , MonadFix m)
- => (URI -> a -> URI)
- -> (URI -> b)
- -> Event t a
- -> m (Dynamic t b)
-route' encode decode routeUpdate = do
- rec rUri <- route (T.decodeUtf8 . U.serializeURIRef' <$> urlUpdates)
- let urlUpdates = attachWith encode (current rUri) routeUpdate
- return $ decode <$> rUri
-
-
--------------------------------------------------------------------------------
--- | Route a single page app according to the part of the path after
--- pathBase
-partialPathRoute
- :: forall t m.
- ( MonadHold t m
- , PostBuild t m
- , DomBuilder t m
- , TriggerEvent t m
- , PerformEvent t m
- , HasJSContext m
- , HasJSContext (Performable m)
- , MonadJSM m
- , MonadJSM (Performable m)
- , MonadFix m)
- => T.Text -- ^ The path segments not related to SPA routing
- -- (leading '/' will be added automaticaly)
- -> Event t T.Text -- ^ Updates to the path segments used for routing
- -- These values will be appended to the base path
- -> m (Dynamic t [T.Text]) -- ^ Path segments used for routing
-partialPathRoute pathBase pathUpdates = do
- route' (flip updateUrl) parseParts pathUpdates
- where
-
- rootPathBase :: T.Text
- rootPathBase =
- if T.null pathBase then
- ""
- else
- "/" <> cleanT pathBase
-
- toPath :: T.Text -> BS.ByteString
- toPath dynpath = T.encodeUtf8 $ rootPathBase <> "/" <> cleanT dynpath
-
- updateUrl :: T.Text -> URI -> URI
- updateUrl updateParts u = u & U.pathL .~ toPath updateParts
-
- parseParts :: URI -> [T.Text]
- parseParts u =
- maybe (error $ pfxErr u pathBase)
- (T.splitOn "/" . T.decodeUtf8 . cleanB) .
- BS.stripPrefix (T.encodeUtf8 $ cleanT pathBase) $
- cleanB (u ^. U.pathL)
-
- cleanT = T.dropWhile (=='/')
- cleanB = BS.dropWhile (== '/')
-
-
--------------------------------------------------------------------------------
-uriOrigin :: U.URIRef U.Absolute -> T.Text
-uriOrigin r = T.decodeUtf8 $ U.serializeURIRef' r'
- where
- r' = r { U.uriPath = mempty
- , U.uriQuery = mempty
- , U.uriFragment = mempty
- }
-
-
--------------------------------------------------------------------------------
-getPopState
- :: forall t m.
- ( MonadHold t m
- , TriggerEvent t m
- , MonadJSM m) => m (Event t URI)
-getPopState = do
- window <- currentWindowUnchecked
- wrapDomEventMaybe window (`on` popState) $ do
- loc <- getLocation window
- locStr <- getHref loc
- return . hush $ U.parseURI U.laxURIParserOptions (T.encodeUtf8 locStr)
-
-
--------------------------------------------------------------------------------
-goForward :: (HasJSContext m, MonadJSM m) => m ()
-goForward = withHistory forward
-
-
--------------------------------------------------------------------------------
-goBack :: (HasJSContext m, MonadJSM m) => m ()
-goBack = withHistory back
-
-
--------------------------------------------------------------------------------
-withHistory :: (HasJSContext m, MonadJSM m) => (History -> m a) -> m a
-withHistory act = do
- w <- currentWindowUnchecked
- h <- getHistory w
- act h
-
-
--------------------------------------------------------------------------------
--- | (Unsafely) get the 'GHCJS.DOM.Location.Location' of a window
-getLoc :: (HasJSContext m, MonadJSM m) => m Location
-getLoc = do
- win <- currentWindowUnchecked
- loc <- getLocation win
- return loc
-
-
--------------------------------------------------------------------------------
--- | (Unsafely) get the URL text of a window
-getUrlText :: (HasJSContext m, MonadJSM m) => m T.Text
-getUrlText = getLoc >>= getHref
-
-
--------------------------------------------------------------------------------
-type URI = U.URIRef U.Absolute
-
-
--------------------------------------------------------------------------------
-getURI :: (HasJSContext m, MonadJSM m) => m URI
-getURI = do
- l <- getUrlText
- return $ either (error "No parse of window location") id .
- U.parseURI U.laxURIParserOptions $ T.encodeUtf8 l
-
-
-dispatchEvent' :: JSM ()
-dispatchEvent' = do
- window <- currentWindowUnchecked
- obj@(Object o) <- JS.create
- JS.objSetPropertyByName obj ("cancelable" :: Text) True
- JS.objSetPropertyByName obj ("bubbles" :: Text) True
- JS.objSetPropertyByName obj ("view" :: Text) window
- event <- JS.jsg ("PopStateEvent" :: Text) >>= ghcjsPure . isFunction >>= \case
- True -> newPopStateEvent ("popstate" :: Text) $ Just $ pFromJSVal o
- False -> do
- doc <- currentDocumentUnchecked
- event <- createEvent doc ("PopStateEvent" :: Text)
- initEvent event ("popstate" :: Text) True True
- JS.objSetPropertyByName obj ("view" :: Text) window
- return $ uncheckedCastTo PopStateEvent event
-
- dispatchEvent_ window event
-
-
--------------------------------------------------------------------------------
-hush :: Either e a -> Maybe a
-hush (Right a) = Just a
-hush _ = Nothing
-
-
--------------------------------------------------------------------------------
-pfxErr :: URI -> T.Text -> String
-pfxErr pn pathBase =
- T.unpack $ "Encountered path (" <> T.decodeUtf8 (U.serializeURIRef' pn)
- <> ") without expected prefix (" <> pathBase <> ")"
diff --git a/client/src/Util/Validation.hs b/client/src/Util/Validation.hs
deleted file mode 100644
index 50f2468..0000000
--- a/client/src/Util/Validation.hs
+++ /dev/null
@@ -1,36 +0,0 @@
-module Util.Validation
- ( nelError
- , toMaybe
- , maybeError
- , fireValidation
- ) where
-
-import Control.Monad (join)
-import Data.List.NonEmpty (NonEmpty)
-import qualified Data.List.NonEmpty as NEL
-import Data.Text (Text)
-import Data.Validation (Validation (Failure, Success))
-import qualified Data.Validation as Validation
-import Reflex.Dom (Dynamic, Event, Reflex)
-import qualified Reflex.Dom as R
-
-nelError :: Validation a b -> Validation (NonEmpty a) b
-nelError = Validation.validation (Failure . NEL.fromList . (:[])) Success
-
-toMaybe :: Validation a b -> Maybe b
-toMaybe (Success s) = Just s
-toMaybe (Failure _) = Nothing
-
-maybeError :: Validation a b -> Maybe a
-maybeError (Success _) = Nothing
-maybeError (Failure e) = Just e
-
-fireValidation
- :: forall t a b c. Reflex t
- => Dynamic t (Validation a b)
- -> Event t c
- -> Event t b
-fireValidation value validate =
- R.fmapMaybe
- (Validation.validation (const Nothing) Just)
- (R.tag (R.current value) validate)
diff --git a/client/src/Util/WaitFor.hs b/client/src/Util/WaitFor.hs
deleted file mode 100644
index fe7b733..0000000
--- a/client/src/Util/WaitFor.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-module Util.WaitFor
- ( waitFor
- ) where
-
-import Data.Time (NominalDiffTime)
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-waitFor
- :: forall t m a b. MonadWidget t m
- => (Event t a -> m (Event t b))
- -> Event t a
- -> m (Event t b, Event t Bool)
-waitFor op input = do
- result <- op input >>= R.debounce (0.5 :: NominalDiffTime)
- let waiting = R.leftmost [ True <$ input , False <$ result ]
- return (result, waiting)
diff --git a/client/src/View/App.hs b/client/src/View/App.hs
deleted file mode 100644
index 71f0234..0000000
--- a/client/src/View/App.hs
+++ /dev/null
@@ -1,108 +0,0 @@
-module View.App
- ( widget
- ) where
-
-import qualified Data.Text as T
-import Prelude hiding (error, init)
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Currency, Init (..), UserId)
-import qualified Common.Msg as Msg
-
-import Model.Route (Route (..))
-import qualified Util.Reflex as ReflexUtil
-import qualified Util.Router as Router
-import qualified View.Category.Category as Category
-import qualified View.Header as Header
-import qualified View.Income.Income as Income
-import qualified View.NotFound as NotFound
-import qualified View.Payment.Payment as Payment
-import qualified View.SignIn as SignIn
-import qualified View.Statistics.Statistics as Statistics
-
-widget :: Maybe Init -> IO ()
-widget init =
- R.mainWidget $ R.divClass "app" $ do
-
- route <- getRoute
-
- rec
- header <- Header.view $ Header.In
- { Header._in_init = initState
- , Header._in_route = route
- }
-
- initState <-
- R.foldDyn
- const
- init
- (R.leftmost $
- [ initEvent
- , Nothing <$ (Header._out_signOut header)
- ])
-
- initEvent <-
- (R.dyn . R.ffor initState $ \case
- Nothing -> do
- signIn <- SignIn.view
- return (Just <$> SignIn._out_success signIn)
-
- Just i -> do
- signedWidget i route
- return R.never) >>= ReflexUtil.flatten
-
- return ()
-
-signedWidget :: forall t m. MonadWidget t m => Init -> Dynamic t Route -> m ()
-signedWidget init route = do
- R.dyn . R.ffor route $ \case
- RootRoute ->
- Payment.view $ Payment.In
- { Payment._in_currentUser = _init_currentUser init
- , Payment._in_currency = _init_currency init
- , Payment._in_users = _init_users init
- }
-
- IncomeRoute ->
- Income.view $ Income.In
- { Income._in_currentUser = _init_currentUser init
- , Income._in_currency = _init_currency init
- , Income._in_users = _init_users init
- }
-
- CategoryRoute ->
- Category.view $ Category.In
- { Category._in_currentUser = _init_currentUser init
- , Category._in_currency = _init_currency init
- , Category._in_users = _init_users init
- }
-
- StatisticsRoute ->
- Statistics.view $ Statistics.In
- { Statistics._in_currency = _init_currency init
- }
-
- NotFoundRoute ->
- NotFound.view
-
- return ()
-
-getRoute :: forall t m. MonadWidget t m => m (Dynamic t Route)
-getRoute = do
- r <- Router.partialPathRoute "" . R.switchPromptlyDyn =<< R.holdDyn R.never R.never
- return . R.ffor r $ \case
- [""] ->
- RootRoute
-
- ["income"] ->
- IncomeRoute
-
- ["category"] ->
- CategoryRoute
-
- ["statistics"] ->
- StatisticsRoute
-
- _ ->
- NotFoundRoute
diff --git a/client/src/View/Category/Category.hs b/client/src/View/Category/Category.hs
deleted file mode 100644
index 5b41bb6..0000000
--- a/client/src/View/Category/Category.hs
+++ /dev/null
@@ -1,94 +0,0 @@
-{-# LANGUAGE ExplicitForAll #-}
-
-module View.Category.Category
- ( view
- , In(..)
- ) where
-
-import Data.Aeson (FromJSON)
-import qualified Data.Maybe as Maybe
-import qualified Data.Text as T
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Category, CategoryPage (..), Currency,
- User, UserId)
-import qualified Common.Msg as Msg
-
-import qualified Component.Button as Button
-import qualified Component.Modal as Modal
-import qualified Component.Pages as Pages
-import Loadable (Loadable (..))
-import qualified Loadable
-import qualified Util.Ajax as AjaxUtil
-import qualified Util.Reflex as ReflexUtil
-import qualified Util.Reflex as ReflexUtil
-import qualified View.Category.Form as Form
-import qualified View.Category.Reducer as Reducer
-import qualified View.Category.Table as Table
-
-data In t = In
- { _in_users :: [User]
- , _in_currentUser :: UserId
- , _in_currency :: Currency
- }
-
-view :: forall t m. MonadWidget t m => In t -> m ()
-view input = do
- rec
- categoryPage <- Reducer.reducer $ Reducer.In
- { Reducer._in_page = page
- , Reducer._in_addCategory = R.leftmost [ headerAddCategory, tableAddCategory ]
- , Reducer._in_editCategory = editCategory
- , Reducer._in_deleteCategory = deleteCategory
- }
-
- let eventFromResult :: forall a. ((Event t (), Table.Out t, Pages.Out t) -> Event t a) -> m (Event t a)
- eventFromResult op = ReflexUtil.flatten $ (Maybe.fromMaybe R.never . fmap op) <$> result
-
- page <- eventFromResult $ Pages._out_newPage . (\(_, _, c) -> c)
- headerAddCategory <- eventFromResult $ (\(a, _, _) -> a)
- tableAddCategory <- eventFromResult $ Table._out_add . (\(_, b, _) -> b)
- editCategory <- eventFromResult $ Table._out_edit . (\(_, b, _) -> b)
- deleteCategory <- eventFromResult $ Table._out_delete . (\(_, b, _) -> b)
-
- result <- Loadable.viewShowValueWhileLoading categoryPage $
- \(CategoryPage page categories usedCategories count) -> do
- header <- headerView
-
- table <- Table.view $ Table.In
- { Table._in_currentUser = _in_currentUser input
- , Table._in_currency = _in_currency input
- , Table._in_categories = categories
- , Table._in_usedCategories = usedCategories
- , Table._in_users = _in_users input
- }
-
- pages <- Pages.view $ Pages.In
- { Pages._in_total = R.constDyn count
- , Pages._in_perPage = Reducer.perPage
- , Pages._in_page = page
- }
-
- return (header, table, pages)
-
- return ()
-
-headerView :: forall t m. MonadWidget t m => m (Event t ())
-headerView =
- R.divClass "withMargin" $
- R.divClass "titleButton" $ do
- R.el "h1" $
- R.text $
- Msg.get Msg.Category_Title
-
- addCategory <- Button._out_clic <$>
- (Button.view . Button.defaultIn . R.text $
- Msg.get Msg.Category_Add)
-
- addCategory <- Modal.view $ Modal.In
- { Modal._in_show = addCategory
- , Modal._in_content = Form.view $ Form.In { Form._in_operation = Form.New }
- }
-
- return addCategory
diff --git a/client/src/View/Category/Form.hs b/client/src/View/Category/Form.hs
deleted file mode 100644
index d91fc2e..0000000
--- a/client/src/View/Category/Form.hs
+++ /dev/null
@@ -1,117 +0,0 @@
-module View.Category.Form
- ( view
- , In(..)
- , Operation(..)
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import Data.Aeson (Value)
-import qualified Data.Aeson as Aeson
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Time.Calendar as Calendar
-import qualified Data.Time.Clock as Time
-import Data.Validation (Validation)
-import qualified Data.Validation as V
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Category (..),
- CreateCategoryForm (..),
- EditCategoryForm (..))
-import qualified Common.Msg as Msg
-import qualified Common.Util.Time as TimeUtil
-import qualified Common.Validation.Category as CategoryValidation
-import qualified Component.Input as Input
-import qualified Component.Modal as Modal
-import qualified Component.ModalForm as ModalForm
-import qualified Util.Ajax as Ajax
-
-data In = In
- { _in_operation :: Operation
- }
-
-data Operation
- = New
- | Clone Category
- | Edit Category
-
-view :: forall t m a. MonadWidget t m => In -> Modal.Content t m
-view input cancel = do
-
- rec
- let reset = R.leftmost
- [ "" <$ ModalForm._out_cancel modalForm
- , "" <$ ModalForm._out_validate modalForm
- , "" <$ cancel
- ]
-
- modalForm <- ModalForm.view $ ModalForm.In
- { ModalForm._in_headerLabel = headerLabel
- , ModalForm._in_ajax = ajax "/api/category"
- , ModalForm._in_form = form reset (ModalForm._out_confirm modalForm)
- }
-
- return (ModalForm._out_hide modalForm, ModalForm._out_validate modalForm)
-
- where
-
- form
- :: Event t String
- -> Event t ()
- -> m (Dynamic t (Validation Text Value))
- form reset confirm = do
- name <- Input._out_raw <$> (Input.view
- (Input.defaultIn
- { Input._in_label = Msg.get Msg.Category_Name
- , Input._in_initialValue = name
- , Input._in_validation = CategoryValidation.name
- })
- (name <$ reset)
- confirm)
-
- color <- Input._out_raw <$> (Input.view
- (Input.defaultIn
- { Input._in_label = Msg.get Msg.Category_Color
- , Input._in_initialValue = color
- , Input._in_inputType = "color"
- , Input._in_hasResetButton = False
- , Input._in_validation = CategoryValidation.color
- })
- (color <$ reset)
- confirm)
-
- return $ do
- n <- name
- c <- color
- return . V.Success $ mkPayload n c
-
- op = _in_operation input
-
- name =
- case op of
- New -> ""
- Clone c -> _category_name c
- Edit c -> _category_name c
-
- color =
- case op of
- New -> ""
- Clone c -> _category_color c
- Edit c -> _category_color c
-
- ajax =
- case op of
- Edit _ -> Ajax.put
- _ -> Ajax.post
-
- headerLabel =
- case op of
- Edit _ -> Msg.get Msg.Category_Edit
- _ -> Msg.get Msg.Category_Add
-
- mkPayload =
- case op of
- Edit i -> \a b -> Aeson.toJSON $ EditCategoryForm (_category_id i) a b
- _ -> \a b -> Aeson.toJSON $ CreateCategoryForm a b
diff --git a/client/src/View/Category/Reducer.hs b/client/src/View/Category/Reducer.hs
deleted file mode 100644
index 5ad0ddb..0000000
--- a/client/src/View/Category/Reducer.hs
+++ /dev/null
@@ -1,59 +0,0 @@
-module View.Category.Reducer
- ( perPage
- , reducer
- , In(..)
- ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (CategoryPage)
-
-import Loadable (Loadable (..))
-import qualified Loadable as Loadable
-import qualified Util.Ajax as AjaxUtil
-import qualified Util.Either as EitherUtil
-
-perPage :: Int
-perPage = 7
-
-data In t a b c = In
- { _in_page :: Event t Int
- , _in_addCategory :: Event t a
- , _in_editCategory :: Event t b
- , _in_deleteCategory :: Event t c
- }
-
-reducer :: forall t m a b c. MonadWidget t m => In t a b c -> m (Dynamic t (Loadable CategoryPage))
-reducer input = do
-
- postBuild <- R.getPostBuild
-
- currentPage <- R.holdDyn 1 (_in_page input)
-
- let loadPage =
- R.leftmost
- [ 1 <$ postBuild
- , _in_page input
- , 1 <$ _in_addCategory input
- , R.tag (R.current currentPage) (_in_editCategory input)
- , R.tag (R.current currentPage) (_in_deleteCategory input)
- ]
-
- getResult <- AjaxUtil.get $ fmap pageUrl loadPage
-
- R.holdDyn
- Loading
- (R.leftmost
- [ Loading <$ loadPage
- , Loadable.fromEither <$> getResult
- ])
-
- where
- pageUrl p =
- "api/categories?page="
- <> (T.pack . show $ p)
- <> "&perPage="
- <> (T.pack . show $ perPage)
diff --git a/client/src/View/Category/Table.hs b/client/src/View/Category/Table.hs
deleted file mode 100644
index 90d013d..0000000
--- a/client/src/View/Category/Table.hs
+++ /dev/null
@@ -1,93 +0,0 @@
-module View.Category.Table
- ( view
- , In(..)
- , Out(..)
- ) where
-
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Category (..), CategoryId, Currency,
- User (..), UserId)
-import qualified Common.Model as CM
-import qualified Common.Msg as Msg
-import qualified Common.View.Format as Format
-
-import qualified Component.ConfirmDialog as ConfirmDialog
-import qualified Component.Table as Table
-import qualified Component.Tag as Tag
-import qualified Util.Ajax as Ajax
-import qualified Util.Either as EitherUtil
-import qualified View.Category.Form as Form
-
-data In t = In
- { _in_currentUser :: UserId
- , _in_currency :: Currency
- , _in_categories :: [Category]
- , _in_usedCategories :: [CategoryId]
- , _in_users :: [User]
- }
-
-data Out t = Out
- { _out_add :: Event t ()
- , _out_edit :: Event t ()
- , _out_delete :: Event t ()
- }
-
-view :: forall t m. MonadWidget t m => In t -> m (Out t)
-view input = do
-
- table <- Table.view $ Table.In
- { Table._in_headerLabel = headerLabel
- , Table._in_rows = _in_categories input
- , Table._in_cell = cell (_in_users input) (_in_currency input)
- , Table._in_cloneModal = \category ->
- Form.view $ Form.In
- { Form._in_operation = Form.Clone category
- }
- , Table._in_editModal = \category ->
- Form.view $ Form.In
- { Form._in_operation = Form.Edit category
- }
- , Table._in_deleteModal = \category ->
- ConfirmDialog.view $ ConfirmDialog.In
- { ConfirmDialog._in_header = Msg.get Msg.Category_DeleteConfirm
- , ConfirmDialog._in_confirm = \e -> do
- res <- Ajax.delete
- (R.constDyn $ T.concat ["/api/category/", T.pack . show $ _category_id category])
- e
- return $ () <$ R.fmapMaybe EitherUtil.eitherToMaybe res
- }
- , Table._in_canEdit = const True
- , Table._in_canDelete = not . flip elem (_in_usedCategories input) . _category_id
- }
-
- return $ Out
- { _out_add = Table._out_add table
- , _out_edit = Table._out_edit table
- , _out_delete = Table._out_delete table
- }
-
-data Header
- = NameHeader
- | ColorHeader
- deriving (Eq, Show, Bounded, Enum)
-
-headerLabel :: Header -> Text
-headerLabel NameHeader = Msg.get Msg.Category_Name
-headerLabel ColorHeader = Msg.get Msg.Category_Color
-
-cell :: forall t m. MonadWidget t m => [User] -> Currency -> Header -> Category -> m ()
-cell users currency header category =
- case header of
- NameHeader ->
- R.text $ _category_name category
-
- ColorHeader ->
- Tag.view $ Tag.In
- { Tag._in_text = _category_name category
- , Tag._in_color = _category_color category
- }
diff --git a/client/src/View/Header.hs b/client/src/View/Header.hs
deleted file mode 100644
index ff9f40a..0000000
--- a/client/src/View/Header.hs
+++ /dev/null
@@ -1,123 +0,0 @@
-module View.Header
- ( view
- , In(..)
- , Out(..)
- ) where
-
-import Data.Map (Map)
-import qualified Data.Map as M
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time (NominalDiffTime)
-import Prelude hiding (error, init)
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Init (..), User (..))
-import qualified Common.Model as CM
-import qualified Common.Msg as Msg
-import qualified Component.Button as Button
-import qualified Component.Link as Link
-import Model.Route (Route (..))
-import qualified Util.Css as CssUtil
-import qualified Util.Reflex as ReflexUtil
-import qualified View.Icon as Icon
-
-data In t = In
- { _in_init :: Dynamic t (Maybe Init)
- , _in_route :: Dynamic t Route
- }
-
-data Out t = Out
- { _out_signOut :: Event t ()
- }
-
-view :: forall t m. MonadWidget t m => (In t) -> m (Out t)
-view input =
- R.el "header" $ do
-
- R.divClass "title" $
- R.text $ Msg.get Msg.App_Title
-
- let showLinks = Maybe.isJust <$> _in_init input
-
- signOut <- R.el "div" $ do
- ReflexUtil.visibleIfDyn showLinks R.blank (links $ _in_route input)
- (R.dyn $ nameSignOut <$> _in_init input) >>= ReflexUtil.flatten
-
- return $ Out
- { _out_signOut = signOut
- }
-
-links :: forall t m. MonadWidget t m => Dynamic t Route -> m ()
-links route = do
- Link.view
- "/"
- (R.ffor route (attrs RootRoute))
- (Msg.get Msg.Payment_Title)
-
- Link.view
- "/income"
- (R.ffor route (attrs IncomeRoute))
- (Msg.get Msg.Income_Title)
-
- Link.view
- "/category"
- (R.ffor route (attrs CategoryRoute))
- (Msg.get Msg.Category_Title)
-
- Link.view
- "/statistics"
- (R.ffor route (attrs StatisticsRoute))
- (Msg.get Msg.Statistics_Title)
-
- where
- attrs linkRoute currentRoute =
- M.singleton "class" $
- CssUtil.classes
- [ ("item", True)
- , ("current", linkRoute == currentRoute)
- ]
-
-nameSignOut :: forall t m. MonadWidget t m => Maybe Init -> m (Event t ())
-nameSignOut init =
- case init of
- Just init -> do
- rec
- attr <- R.holdDyn
- (M.singleton "class" "nameSignOut")
- (fmap (const $ M.fromList [("style", "visibility: hidden"), ("class", "nameSignOut")]) signOut)
-
- signOut <- R.elDynAttr "nameSignOut" attr $ do
- case CM.findUser (_init_currentUser init) (_init_users init) of
- Just user -> R.divClass "name" $ R.text (_user_name user)
- Nothing -> R.blank
- signOutButton
-
- return signOut
- _ ->
- return R.never
-
-signOutButton :: forall t m. MonadWidget t m => m (Event t ())
-signOutButton = do
- rec
- signOut <- Button.view $
- (Button.defaultIn Icon.signOut)
- { Button._in_class = R.constDyn "signOut item"
- , Button._in_waiting = waiting
- }
- let signOutClic = Button._out_clic signOut
- waiting = R.leftmost
- [ fmap (const True) signOutClic
- , fmap (const False) signOutSuccess
- ]
- signOutSuccess <- askSignOut signOutClic >>= R.debounce (0.5 :: NominalDiffTime)
-
- return . fmap (const ()) . R.ffilter (== True) $ signOutSuccess
-
- where askSignOut :: forall t m. MonadWidget t m => Event t () -> m (Event t Bool)
- askSignOut signOut =
- fmap getResult <$> R.performRequestAsync xhrRequest
- where xhrRequest = fmap (const $ R.postJson "/api/signOut" ()) signOut
- getResult = (== 200) . R._xhrResponse_status
diff --git a/client/src/View/Icon.hs b/client/src/View/Icon.hs
deleted file mode 100644
index cc2ef3f..0000000
--- a/client/src/View/Icon.hs
+++ /dev/null
@@ -1,71 +0,0 @@
-module View.Icon
- ( clone
- , cross
- , delete
- , edit
- , loading
- , doubleLeft
- , doubleLeftBar
- , doubleRight
- , doubleRightBar
- , signOut
- ) where
-
-import Data.Map (Map)
-import qualified Data.Map as M
-import Data.Text (Text)
-import Reflex.Dom (MonadWidget)
-import qualified Reflex.Dom as R
-
-clone :: forall t m. MonadWidget t m => m ()
-clone =
- svgAttr "svg" (M.fromList [ ("width", "24"), ("height", "24"), ("viewBox", "0 0 24 24") ]) $
- svgAttr "path" (M.fromList [("d", "M15.143 13.244l.837-2.244 2.698 5.641-5.678 2.502.805-2.23s-8.055-3.538-7.708-10.913c2.715 5.938 9.046 7.244 9.046 7.244zm8.857-7.244v18h-18v-6h-6v-18h18v6h6zm-2 2h-12.112c-.562-.578-1.08-1.243-1.521-2h7.633v-4h-14v14h4v-3.124c.6.961 1.287 1.823 2 2.576v6.548h14v-14z")]) $ R.blank
-
-cross :: forall t m. MonadWidget t m => m ()
-cross =
- svgAttr "svg" (M.fromList [ ("width", "15"), ("height", "15"), ("viewBox", "0 0 1792 1792") ]) $
- svgAttr "path" (M.fromList [("d", "M1490 1322q0 40-28 68l-136 136q-28 28-68 28t-68-28l-294-294-294 294q-28 28-68 28t-68-28l-136-136q-28-28-28-68t28-68l294-294-294-294q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 294 294-294q28-28 68-28t68 28l136 136q28 28 28 68t-28 68l-294 294 294 294q28 28 28 68z")]) $ R.blank
-
-delete :: forall t m. MonadWidget t m => m ()
-delete =
- svgAttr "svg" (M.fromList [ ("width", "18"), ("height", "18"), ("viewBox", "0 0 1792 1792") ]) $
- svgAttr "path" (M.fromList [("d", "M704 1376v-704q0-14-9-23t-23-9h-64q-14 0-23 9t-9 23v704q0 14 9 23t23 9h64q14 0 23-9t9-23zm256 0v-704q0-14-9-23t-23-9h-64q-14 0-23 9t-9 23v704q0 14 9 23t23 9h64q14 0 23-9t9-23zm256 0v-704q0-14-9-23t-23-9h-64q-14 0-23 9t-9 23v704q0 14 9 23t23 9h64q14 0 23-9t9-23zm-544-992h448l-48-117q-7-9-17-11h-317q-10 2-17 11zm928 32v64q0 14-9 23t-23 9h-96v948q0 83-47 143.5t-113 60.5h-832q-66 0-113-58.5t-47-141.5v-952h-96q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h309l70-167q15-37 54-63t79-26h320q40 0 79 26t54 63l70 167h309q14 0 23 9t9 23z")]) $ R.blank
-
-doubleLeft :: forall t m. MonadWidget t m => m ()
-doubleLeft =
- svgAttr "svg" (M.fromList [ ("width", "13"), ("height", "13"), ("viewBox", "0 0 1792 1792") ]) $
- svgAttr "path" (M.fromList [("d", "M1683 141q19-19 32-13t13 32v1472q0 26-13 32t-32-13l-710-710q-8-9-13-19v710q0 26-13 32t-32-13l-710-710q-19-19-19-45t19-45l710-710q19-19 32-13t13 32v710q5-11 13-19z")]) $ R.blank
-
-doubleLeftBar :: forall t m. MonadWidget t m => m ()
-doubleLeftBar =
- svgAttr "svg" (M.fromList [ ("width", "13"), ("height", "13"), ("viewBox", "0 0 1792 1792") ]) $
- svgAttr "path" (M.fromList [("d", "M1747 141q19-19 32-13t13 32v1472q0 26-13 32t-32-13l-710-710q-9-9-13-19v710q0 26-13 32t-32-13l-710-710q-9-9-13-19v678q0 26-19 45t-45 19h-128q-26 0-45-19t-19-45v-1408q0-26 19-45t45-19h128q26 0 45 19t19 45v678q4-11 13-19l710-710q19-19 32-13t13 32v710q4-11 13-19z")]) $ R.blank
-
-doubleRight :: forall t m. MonadWidget t m => m ()
-doubleRight =
- svgAttr "svg" (M.fromList [ ("width", "13"), ("height", "13"), ("viewBox", "0 0 1792 1792") ]) $
- svgAttr "path" (M.fromList [("d", "M109 1651q-19 19-32 13t-13-32v-1472q0-26 13-32t32 13l710 710q8 8 13 19v-710q0-26 13-32t32 13l710 710q19 19 19 45t-19 45l-710 710q-19 19-32 13t-13-32v-710q-5 10-13 19z")]) $ R.blank
-
-doubleRightBar :: forall t m. MonadWidget t m => m ()
-doubleRightBar =
- svgAttr "svg" (M.fromList [ ("width", "13"), ("height", "13"), ("viewBox", "0 0 1792 1792") ]) $
- svgAttr "path" (M.fromList [("d", "M45 1651q-19 19-32 13t-13-32v-1472q0-26 13-32t32 13l710 710q8 8 13 19v-710q0-26 13-32t32 13l710 710q8 8 13 19v-678q0-26 19-45t45-19h128q26 0 45 19t19 45v1408q0 26-19 45t-45 19h-128q-26 0-45-19t-19-45v-678q-5 10-13 19l-710 710q-19 19-32 13t-13-32v-710q-5 10-13 19z")]) $ R.blank
-
-edit :: forall t m. MonadWidget t m => m ()
-edit =
- svgAttr "svg" (M.fromList [ ("width", "18"), ("height", "18"), ("viewBox", "0 0 1792 1792") ]) $
- svgAttr "path" (M.fromList [("d", "M491 1536l91-91-235-235-91 91v107h128v128h107zm523-928q0-22-22-22-10 0-17 7l-542 542q-7 7-7 17 0 22 22 22 10 0 17-7l542-542q7-7 7-17zm-54-192l416 416-832 832h-416v-416zm683 96q0 53-37 90l-166 166-416-416 166-165q36-38 90-38 53 0 91 38l235 234q37 39 37 91z")]) $ R.blank
-
-loading :: forall t m. MonadWidget t m => m ()
-loading =
- svgAttr "svg" (M.fromList [ ("width", "24"), ("height", "24"), ("viewBox", "0 0 24 24"), ("class", "loader"), ("fill", "currentColor") ]) $
- svgAttr "path" (M.fromList [("d", "M13.75 22c0 .966-.783 1.75-1.75 1.75s-1.75-.784-1.75-1.75.783-1.75 1.75-1.75 1.75.784 1.75 1.75zm-1.75-22c-1.104 0-2 .896-2 2s.896 2 2 2 2-.896 2-2-.896-2-2-2zm10 10.75c.689 0 1.249.561 1.249 1.25 0 .69-.56 1.25-1.249 1.25-.69 0-1.249-.559-1.249-1.25 0-.689.559-1.25 1.249-1.25zm-22 1.25c0 1.105.896 2 2 2s2-.895 2-2c0-1.104-.896-2-2-2s-2 .896-2 2zm19-8c.551 0 1 .449 1 1 0 .553-.449 1.002-1 1-.551 0-1-.447-1-.998 0-.553.449-1.002 1-1.002zm0 13.5c.828 0 1.5.672 1.5 1.5s-.672 1.501-1.502 1.5c-.826 0-1.498-.671-1.498-1.499 0-.829.672-1.501 1.5-1.501zm-14-14.5c1.104 0 2 .896 2 2s-.896 2-2.001 2c-1.103 0-1.999-.895-1.999-2s.896-2 2-2zm0 14c1.104 0 2 .896 2 2s-.896 2-2.001 2c-1.103 0-1.999-.895-1.999-2s.896-2 2-2z")]) $ R.blank
-
-signOut :: forall t m. MonadWidget t m => m ()
-signOut =
- svgAttr "svg" (M.fromList [ ("width", "30"), ("height", "30"), ("viewBox", "0 0 1792 1792") ]) $
- svgAttr "path" (M.fromList [("d", "M1664 896q0 156-61 298t-164 245-245 164-298 61-298-61-245-164-164-245-61-298q0-182 80.5-343t226.5-270q43-32 95.5-25t83.5 50q32 42 24.5 94.5t-49.5 84.5q-98 74-151.5 181t-53.5 228q0 104 40.5 198.5t109.5 163.5 163.5 109.5 198.5 40.5 198.5-40.5 163.5-109.5 109.5-163.5 40.5-198.5q0-121-53.5-228t-151.5-181q-42-32-49.5-84.5t24.5-94.5q31-43 84-50t95 25q146 109 226.5 270t80.5 343zm-640-768v640q0 52-38 90t-90 38-90-38-38-90v-640q0-52 38-90t90-38 90 38 38 90z")]) $ R.blank
-
-svgAttr :: forall t m a. MonadWidget t m => Text -> Map Text Text -> m a -> m a
-svgAttr elementTag attrs child = R.elWith elementTag (R.ElConfig (Just "http://www.w3.org/2000/svg") attrs) child
diff --git a/client/src/View/Income/Form.hs b/client/src/View/Income/Form.hs
deleted file mode 100644
index 59f6a0d..0000000
--- a/client/src/View/Income/Form.hs
+++ /dev/null
@@ -1,119 +0,0 @@
-module View.Income.Form
- ( view
- , In(..)
- , Operation(..)
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import Data.Aeson (Value)
-import qualified Data.Aeson as Aeson
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Time.Calendar as Calendar
-import qualified Data.Time.Clock as Time
-import Data.Validation (Validation)
-import qualified Data.Validation as V
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (CreateIncomeForm (..),
- EditIncomeForm (..), Income (..))
-import qualified Common.Msg as Msg
-import qualified Common.Util.Time as TimeUtil
-import qualified Common.Validation.Income as IncomeValidation
-import qualified Component.Input as Input
-import qualified Component.Modal as Modal
-import qualified Component.ModalForm as ModalForm
-import qualified Util.Ajax as Ajax
-
-data In = In
- { _in_operation :: Operation
- }
-
-data Operation
- = New
- | Clone Income
- | Edit Income
-
-view :: forall t m a. MonadWidget t m => In -> Modal.Content t m
-view input cancel = do
-
- rec
- let reset = R.leftmost
- [ "" <$ ModalForm._out_cancel modalForm
- , "" <$ ModalForm._out_validate modalForm
- , "" <$ cancel
- ]
-
- modalForm <- ModalForm.view $ ModalForm.In
- { ModalForm._in_headerLabel = headerLabel
- , ModalForm._in_ajax = ajax "/api/income"
- , ModalForm._in_form = form reset (ModalForm._out_confirm modalForm)
- }
-
- return (ModalForm._out_hide modalForm, ModalForm._out_validate modalForm)
-
- where
-
- form
- :: Event t String
- -> Event t ()
- -> m (Dynamic t (Validation Text Value))
- form reset confirm = do
- amount <- Input._out_raw <$> (Input.view
- (Input.defaultIn
- { Input._in_label = Msg.get Msg.Income_Amount
- , Input._in_initialValue = amount
- , Input._in_validation = IncomeValidation.amount
- })
- (amount <$ reset)
- confirm)
-
- currentDay <- liftIO $ Time.getCurrentTime >>= TimeUtil.timeToDay
-
- let initialDate = T.pack . Calendar.showGregorian $ date currentDay
-
- date <- Input._out_raw <$> (Input.view
- (Input.defaultIn
- { Input._in_label = Msg.get Msg.Income_Date
- , Input._in_initialValue = initialDate
- , Input._in_inputType = "date"
- , Input._in_hasResetButton = False
- , Input._in_validation = IncomeValidation.date
- })
- (initialDate <$ reset)
- confirm)
-
- return $ do
- a <- amount
- d <- date
- return . V.Success $ mkPayload a d
-
- op = _in_operation input
-
- amount =
- case op of
- New -> ""
- Clone i -> T.pack . show . _income_amount $ i
- Edit i -> T.pack . show . _income_amount $ i
-
- date currentDay =
- case op of
- Edit i -> _income_date i
- _ -> currentDay
-
- ajax =
- case op of
- Edit _ -> Ajax.put
- _ -> Ajax.post
-
- headerLabel =
- case op of
- Edit _ -> Msg.get Msg.Income_Edit
- _ -> Msg.get Msg.Income_AddLong
-
- mkPayload =
- case op of
- Edit i -> \a b -> Aeson.toJSON $ EditIncomeForm (_income_id i) a b
- _ -> \a b -> Aeson.toJSON $ CreateIncomeForm a b
diff --git a/client/src/View/Income/Header.hs b/client/src/View/Income/Header.hs
deleted file mode 100644
index a26e16a..0000000
--- a/client/src/View/Income/Header.hs
+++ /dev/null
@@ -1,77 +0,0 @@
-module View.Income.Header
- ( view
- , In(..)
- , Out(..)
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import qualified Data.Map as M
-import qualified Data.Maybe as Maybe
-import qualified Data.Text as T
-import qualified Data.Time.Clock as Clock
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Currency, Income (..),
- IncomeHeader (..), User (..))
-import qualified Common.Model as CM
-import qualified Common.Msg as Msg
-import qualified Common.View.Format as Format
-
-import qualified Component.Button as Button
-import qualified Component.Modal as Modal
-import qualified View.Income.Form as Form
-
-data In t = In
- { _in_users :: [User]
- , _in_header :: IncomeHeader
- , _in_currency :: Currency
- }
-
-data Out t = Out
- { _out_add :: Event t ()
- }
-
-view :: forall t m. MonadWidget t m => In t -> m (Out t)
-view input =
- R.divClass "withMargin" $ do
-
- currentTime <- liftIO Clock.getCurrentTime
-
- case _incomeHeader_since $ _in_header input of
- Nothing ->
- R.blank
-
- Just since ->
- R.el "div" $ do
-
- R.el "h1" $ do
- R.text $ Msg.get (Msg.Income_CumulativeSince (Format.longDay since))
-
- R.el "ul" $
- flip mapM_ (M.toList . _incomeHeader_byUser $ _in_header input) $ \(userId, amount) ->
- R.el "li" $
- R.text $
- T.intercalate " "
- [ Maybe.fromMaybe "" . fmap _user_name $ CM.findUser userId (_in_users input)
- , "−"
- , Format.price (_in_currency input) amount
- ]
-
- R.divClass "titleButton" $ do
- R.el "h1" $
- R.text $
- Msg.get Msg.Income_MonthlyNet
-
- addIncome <- Button._out_clic <$>
- (Button.view . Button.defaultIn . R.text $
- Msg.get Msg.Income_AddLong)
-
- addIncome <- Modal.view $ Modal.In
- { Modal._in_show = addIncome
- , Modal._in_content = Form.view $ Form.In { Form._in_operation = Form.New }
- }
-
- return $ Out
- { _out_add = addIncome
- }
diff --git a/client/src/View/Income/Income.hs b/client/src/View/Income/Income.hs
deleted file mode 100644
index 7be8091..0000000
--- a/client/src/View/Income/Income.hs
+++ /dev/null
@@ -1,75 +0,0 @@
-{-# LANGUAGE ExplicitForAll #-}
-
-module View.Income.Income
- ( view
- , In(..)
- ) where
-
-import Data.Aeson (FromJSON)
-import qualified Data.Maybe as Maybe
-import qualified Data.Text as T
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Currency, Income (..), IncomePage (..),
- User, UserId)
-
-import qualified Component.Pages as Pages
-import Loadable (Loadable (..))
-import qualified Loadable
-import qualified Util.Ajax as AjaxUtil
-import qualified Util.Reflex as ReflexUtil
-import qualified Util.Reflex as ReflexUtil
-import qualified View.Income.Header as Header
-import qualified View.Income.Reducer as Reducer
-import qualified View.Income.Table as Table
-
-data In t = In
- { _in_users :: [User]
- , _in_currentUser :: UserId
- , _in_currency :: Currency
- }
-
-view :: forall t m. MonadWidget t m => In t -> m ()
-view input = do
- rec
- incomePage <- Reducer.reducer $ Reducer.In
- { Reducer._in_page = page
- , Reducer._in_addIncome = R.leftmost [headerAddIncome, tableAddIncome]
- , Reducer._in_editIncome = editIncome
- , Reducer._in_deleteIncome = deleteIncome
- }
-
- let eventFromResult :: forall a. ((Header.Out t, Table.Out t, Pages.Out t) -> Event t a) -> m (Event t a)
- eventFromResult op = ReflexUtil.flatten . fmap (Maybe.fromMaybe R.never . fmap op) $ result
-
- page <- eventFromResult $ Pages._out_newPage . (\(_, _, c) -> c)
- headerAddIncome <- eventFromResult $ Header._out_add . (\(a, _, _) -> a)
- tableAddIncome <- eventFromResult $ Table._out_add . (\(_, b, _) -> b)
- editIncome <- eventFromResult $ Table._out_edit . (\(_, b, _) -> b)
- deleteIncome <- eventFromResult $ Table._out_delete . (\(_, b, _) -> b)
-
- result <- Loadable.viewShowValueWhileLoading incomePage $
- \(IncomePage page header incomes count) -> do
- header <- Header.view $ Header.In
- { Header._in_users = _in_users input
- , Header._in_header = header
- , Header._in_currency = _in_currency input
- }
-
- table <- Table.view $ Table.In
- { Table._in_currentUser = _in_currentUser input
- , Table._in_currency = _in_currency input
- , Table._in_incomes = incomes
- , Table._in_users = _in_users input
- }
-
- pages <- Pages.view $ Pages.In
- { Pages._in_total = R.constDyn count
- , Pages._in_perPage = Reducer.perPage
- , Pages._in_page = page
- }
-
- return (header, table, pages)
-
- return ()
diff --git a/client/src/View/Income/Reducer.hs b/client/src/View/Income/Reducer.hs
deleted file mode 100644
index ea9f664..0000000
--- a/client/src/View/Income/Reducer.hs
+++ /dev/null
@@ -1,59 +0,0 @@
-module View.Income.Reducer
- ( perPage
- , reducer
- , In(..)
- ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (IncomePage)
-
-import Loadable (Loadable (..))
-import qualified Loadable as Loadable
-import qualified Util.Ajax as AjaxUtil
-import qualified Util.Either as EitherUtil
-
-perPage :: Int
-perPage = 7
-
-data In t a b c = In
- { _in_page :: Event t Int
- , _in_addIncome :: Event t a
- , _in_editIncome :: Event t b
- , _in_deleteIncome :: Event t c
- }
-
-reducer :: forall t m a b c. MonadWidget t m => In t a b c -> m (Dynamic t (Loadable IncomePage))
-reducer input = do
-
- postBuild <- R.getPostBuild
-
- currentPage <- R.holdDyn 1 (_in_page input)
-
- let loadPage =
- R.leftmost
- [ 1 <$ postBuild
- , _in_page input
- , 1 <$ _in_addIncome input
- , R.tag (R.current currentPage) (_in_editIncome input)
- , R.tag (R.current currentPage) (_in_deleteIncome input)
- ]
-
- getResult <- AjaxUtil.get $ fmap pageUrl loadPage
-
- R.holdDyn
- Loading
- (R.leftmost
- [ Loading <$ loadPage
- , Loadable.fromEither <$> getResult
- ])
-
- where
- pageUrl p =
- "api/incomes?page="
- <> (T.pack . show $ p)
- <> "&perPage="
- <> (T.pack . show $ perPage)
diff --git a/client/src/View/Income/Table.hs b/client/src/View/Income/Table.hs
deleted file mode 100644
index 7b7940d..0000000
--- a/client/src/View/Income/Table.hs
+++ /dev/null
@@ -1,93 +0,0 @@
-module View.Income.Table
- ( view
- , In(..)
- , Out(..)
- ) where
-
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Currency, Income (..), User (..),
- UserId)
-import qualified Common.Model as CM
-import qualified Common.Msg as Msg
-import qualified Common.View.Format as Format
-
-import qualified Component.ConfirmDialog as ConfirmDialog
-import qualified Component.Table as Table
-import qualified Util.Ajax as Ajax
-import qualified Util.Either as EitherUtil
-import qualified View.Income.Form as Form
-
-data In t = In
- { _in_currentUser :: UserId
- , _in_currency :: Currency
- , _in_incomes :: [Income]
- , _in_users :: [User]
- }
-
-data Out t = Out
- { _out_add :: Event t ()
- , _out_edit :: Event t ()
- , _out_delete :: Event t ()
- }
-
-view :: forall t m. MonadWidget t m => In t -> m (Out t)
-view input = do
-
- table <- Table.view $ Table.In
- { Table._in_headerLabel = headerLabel
- , Table._in_rows = _in_incomes input
- , Table._in_cell = cell (_in_users input) (_in_currency input)
- , Table._in_cloneModal = \income ->
- Form.view $ Form.In
- { Form._in_operation = Form.Clone income
- }
- , Table._in_editModal = \income ->
- Form.view $ Form.In
- { Form._in_operation = Form.Edit income
- }
- , Table._in_deleteModal = \income ->
- ConfirmDialog.view $ ConfirmDialog.In
- { ConfirmDialog._in_header = Msg.get Msg.Income_DeleteConfirm
- , ConfirmDialog._in_confirm = \e -> do
- res <- Ajax.delete
- (R.constDyn $ T.concat ["/api/income/", T.pack . show $ _income_id income])
- e
- return $ () <$ R.fmapMaybe EitherUtil.eitherToMaybe res
- }
- , Table._in_canEdit = (== (_in_currentUser input)) . _income_userId
- , Table._in_canDelete = (== (_in_currentUser input)) . _income_userId
- }
-
- return $ Out
- { _out_add = Table._out_add table
- , _out_edit = Table._out_edit table
- , _out_delete = Table._out_delete table
- }
-
-data Header
- = UserHeader
- | AmountHeader
- | DateHeader
- deriving (Eq, Show, Bounded, Enum)
-
-headerLabel :: Header -> Text
-headerLabel UserHeader = Msg.get Msg.Income_Name
-headerLabel DateHeader = Msg.get Msg.Income_Date
-headerLabel AmountHeader = Msg.get Msg.Income_Amount
-
-cell :: forall t m. MonadWidget t m => [User] -> Currency -> Header -> Income -> m ()
-cell users currency header income =
- case header of
- UserHeader ->
- R.text . Maybe.fromMaybe "" . fmap _user_name $ CM.findUser (_income_userId income) users
-
- DateHeader ->
- R.text . Format.longDay . _income_date $ income
-
- AmountHeader ->
- R.text . Format.price currency . _income_amount $ income
diff --git a/client/src/View/NotFound.hs b/client/src/View/NotFound.hs
deleted file mode 100644
index 1597849..0000000
--- a/client/src/View/NotFound.hs
+++ /dev/null
@@ -1,20 +0,0 @@
-module View.NotFound
- ( view
- ) where
-
-import qualified Data.Map as M
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import qualified Common.Msg as Msg
-import qualified Component.Link as Link
-
-view :: forall t m. MonadWidget t m => m ()
-view =
- R.divClass "notfound" $ do
- R.text (Msg.get Msg.NotFound_Message)
-
- Link.view
- "/"
- (R.constDyn $ M.singleton "class" "link")
- (Msg.get Msg.NotFound_LinkMessage)
diff --git a/client/src/View/Payment/Form.hs b/client/src/View/Payment/Form.hs
deleted file mode 100644
index 6c31fad..0000000
--- a/client/src/View/Payment/Form.hs
+++ /dev/null
@@ -1,199 +0,0 @@
-module View.Payment.Form
- ( view
- , In(..)
- , Operation(..)
- ) where
-
-import Control.Monad (join)
-import Control.Monad.IO.Class (liftIO)
-import Data.Aeson (Value)
-import qualified Data.Aeson as Aeson
-import qualified Data.List as L
-import Data.List.NonEmpty (NonEmpty)
-import qualified Data.Map as M
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time (NominalDiffTime)
-import Data.Time.Calendar (Day)
-import qualified Data.Time.Calendar as Calendar
-import qualified Data.Time.Clock as Clock
-import Data.Validation (Validation)
-import qualified Data.Validation as V
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-import qualified Text.Read as T
-
-import Common.Model (Category (..), CategoryId,
- CreatePaymentForm (..),
- EditPaymentForm (..),
- Frequency (..), Payment (..))
-import qualified Common.Msg as Msg
-import qualified Common.Util.Time as TimeUtil
-import qualified Common.Validation.Payment as PaymentValidation
-
-import qualified Component.Input as Input
-import qualified Component.Modal as Modal
-import qualified Component.ModalForm as ModalForm
-import qualified Component.Select as Select
-import qualified Util.Ajax as Ajax
-import qualified Util.Either as EitherUtil
-import qualified Util.Validation as ValidationUtil
-
-data In t = In
- { _in_categories :: [Category]
- , _in_operation :: Operation t
- , _in_frequency :: Frequency
- }
-
-data Operation t
- = New
- | Clone Payment
- | Edit Payment
-
-view :: forall t m a. MonadWidget t m => In t -> Modal.Content t m
-view input cancel = do
- rec
- let reset = R.leftmost
- [ "" <$ ModalForm._out_cancel modalForm
- , "" <$ ModalForm._out_validate modalForm
- , "" <$ cancel
- ]
-
- modalForm <- ModalForm.view $ ModalForm.In
- { ModalForm._in_headerLabel = headerLabel
- , ModalForm._in_ajax = ajax "/api/payment"
- , ModalForm._in_form = form reset (ModalForm._out_confirm modalForm)
- }
-
- return (ModalForm._out_hide modalForm, ModalForm._out_validate modalForm)
-
- where
-
- form
- :: Event t String
- -> Event t ()
- -> m (Dynamic t (Validation (NonEmpty Text) Value))
- form reset confirm = do
- name <- Input.view
- (Input.defaultIn
- { Input._in_label = Msg.get Msg.Payment_Name
- , Input._in_initialValue = name
- , Input._in_validation = PaymentValidation.name
- })
- (name <$ reset)
- confirm
-
- cost <- Input._out_raw <$> (Input.view
- (Input.defaultIn
- { Input._in_label = Msg.get Msg.Payment_Cost
- , Input._in_initialValue = cost
- , Input._in_validation = PaymentValidation.cost
- })
- (cost <$ reset)
- confirm)
-
- currentDate <- date
-
- date <-
- case frequency of
- Punctual -> do
- Input._out_raw <$> (Input.view
- (Input.defaultIn
- { Input._in_label = Msg.get Msg.Payment_Date
- , Input._in_initialValue = currentDate
- , Input._in_inputType = "date"
- , Input._in_hasResetButton = False
- , Input._in_validation = PaymentValidation.date
- })
- (currentDate <$ reset)
- confirm)
- Monthly ->
- return . R.constDyn $ currentDate
-
- setCategory <-
- R.debounce (1 :: NominalDiffTime) (R.updated $ Input._out_raw name)
- >>= (return . R.ffilter (\name -> T.length name >= 3))
- >>= (Ajax.get . (fmap ("/api/payment/category?name=" <>)))
- >>= (return . R.mapMaybe (join . EitherUtil.eitherToMaybe))
-
- category <- Select._out_value <$> (Select.view $ Select.In
- { Select._in_label = Msg.get Msg.Payment_Category
- , Select._in_initialValue = category
- , Select._in_value = setCategory
- , Select._in_values = R.constDyn categories
- , Select._in_reset = category <$ reset
- , Select._in_isValid = PaymentValidation.category (map _category_id $ _in_categories input)
- , Select._in_validate = confirm
- })
-
- return $ do
- n <- Input._out_value name
- c <- cost
- d <- date
- cat <- category
- return (mkPayload
- <$> ValidationUtil.nelError n
- <*> V.Success c
- <*> V.Success d
- <*> ValidationUtil.nelError cat
- <*> V.Success frequency)
-
- frequencies =
- M.fromList
- [ (Punctual, Msg.get Msg.Payment_PunctualMale)
- , (Monthly, Msg.get Msg.Payment_MonthlyMale)
- ]
-
- categories = M.fromList . flip map (_in_categories input) $ \c ->
- (_category_id c, _category_name c)
-
- category =
- case op of
- New -> -1
- Clone p -> _payment_category p
- Edit p -> _payment_category p
-
- op = _in_operation input
-
- name =
- case op of
- New -> ""
- Clone p -> _payment_name p
- Edit p -> _payment_name p
-
- cost =
- case op of
- New -> ""
- Clone p -> T.pack . show . _payment_cost $ p
- Edit p -> T.pack . show . _payment_cost $ p
-
- date = do
- currentDay <- liftIO $ Clock.getCurrentTime >>= TimeUtil.timeToDay
- return . T.pack . Calendar.showGregorian $
- case op of
- New -> currentDay
- Clone p -> currentDay
- Edit p -> _payment_date p
-
- frequency =
- case op of
- New -> _in_frequency input
- Clone p -> _payment_frequency p
- Edit p -> _payment_frequency p
-
- headerLabel =
- case op of
- New -> Msg.get Msg.Payment_Add
- Clone _ -> Msg.get Msg.Payment_CloneLong
- Edit _ -> Msg.get Msg.Payment_EditLong
-
- ajax =
- case op of
- Edit _ -> Ajax.put
- _ -> Ajax.post
-
- mkPayload =
- case op of
- Edit p -> \a b c d e -> Aeson.toJSON $ EditPaymentForm (_payment_id p) a b c d e
- _ -> \a b c d e -> Aeson.toJSON $ CreatePaymentForm a b c d e
diff --git a/client/src/View/Payment/HeaderForm.hs b/client/src/View/Payment/HeaderForm.hs
deleted file mode 100644
index 1915841..0000000
--- a/client/src/View/Payment/HeaderForm.hs
+++ /dev/null
@@ -1,85 +0,0 @@
-module View.Payment.HeaderForm
- ( view
- , In(..)
- , Out(..)
- ) where
-
-import qualified Data.Map as M
-import Data.Text (Text)
-import qualified Data.Validation as V
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Category, Currency, Frequency (..),
- Income (..), Payment (..), User (..))
-import qualified Common.Msg as Msg
-
-import qualified Component.Button as Button
-import qualified Component.Input as Input
-import qualified Component.Modal as Modal
-import qualified Component.Select as Select
-import qualified Util.Reflex as ReflexUtil
-import qualified View.Payment.Form as Form
-
-data In t = In
- { _in_reset :: Event t ()
- , _in_categories :: [Category]
- }
-
-data Out t = Out
- { _out_search :: Event t Text
- , _out_frequency :: Event t Frequency
- , _out_addPayment :: Event t ()
- }
-
-view :: forall t m. MonadWidget t m => In t -> m (Out t)
-view input =
- R.divClass "g-PaymentHeaderForm" $ do
-
- (searchName, frequency) <- R.el "div" $ do
-
- searchName <- Input._out_raw <$> (Input.view
- ( Input.defaultIn { Input._in_label = Msg.get Msg.Search_Name })
- ("" <$ _in_reset input)
- R.never)
-
- let frequencies = M.fromList
- [ (Punctual, Msg.get Msg.Payment_PunctualMale)
- , (Monthly, Msg.get Msg.Payment_MonthlyMale)
- ]
-
- frequency <- Select._out_raw <$> (Select.view $ Select.In
- { Select._in_label = ""
- , Select._in_initialValue = Punctual
- , Select._in_value = R.never
- , Select._in_values = R.constDyn frequencies
- , Select._in_reset = R.never
- , Select._in_isValid = V.Success
- , Select._in_validate = R.never
- })
-
- return (searchName, frequency)
-
- addPaymentButton <- Button._out_clic <$>
- (Button.view $
- (Button.defaultIn (R.text $ Msg.get Msg.Payment_Add))
- { Button._in_class = R.constDyn "addPayment"
- })
-
- addPayment <-
- (R.dyn . R.ffor frequency $ \frequency ->
- Modal.view $ Modal.In
- { Modal._in_show = addPaymentButton
- , Modal._in_content =
- Form.view $ Form.In
- { Form._in_categories = _in_categories input
- , Form._in_operation = Form.New
- , Form._in_frequency = frequency
- }
- }) >>= ReflexUtil.flatten
-
- return $ Out
- { _out_search = R.updated searchName
- , _out_frequency = R.updated frequency
- , _out_addPayment = addPayment
- }
diff --git a/client/src/View/Payment/HeaderInfos.hs b/client/src/View/Payment/HeaderInfos.hs
deleted file mode 100644
index f84ee1f..0000000
--- a/client/src/View/Payment/HeaderInfos.hs
+++ /dev/null
@@ -1,94 +0,0 @@
-module View.Payment.HeaderInfos
- ( view
- , In(..)
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import qualified Data.List as L hiding (groupBy)
-import Data.Map (Map)
-import qualified Data.Map as M
-import Data.Maybe (fromMaybe)
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Time as Time
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Currency, ExceedingPayer (..),
- Payment (..), PaymentHeader (..),
- User (..), UserId)
-import qualified Common.Model as CM
-import qualified Common.Msg as Msg
-import qualified Common.View.Format as Format
-
-data In t = In
- { _in_users :: [User]
- , _in_currency :: Currency
- , _in_header :: PaymentHeader
- , _in_paymentCount :: Int
- }
-
-view :: forall t m. MonadWidget t m => In t -> m ()
-view input =
- R.divClass "g-PaymentHeaderInfos" $ do
- exceedingPayers
- (_in_users input)
- (_in_currency input)
- (_paymentHeader_exceedingPayers header)
-
- infos
- (_in_users input)
- (_in_currency input)
- (_paymentHeader_repartition header)
- (_in_paymentCount input)
-
- where
- header = _in_header input
-
-exceedingPayers
- :: forall t m. MonadWidget t m
- => [User]
- -> Currency
- -> [ExceedingPayer]
- -> m ()
-exceedingPayers users currency payers =
- R.divClass "g-PaymentHeaderInfos__ExceedingPayers" $
- flip mapM_ payers $ \payer ->
- R.elClass "span" "exceedingPayer" $ do
- R.elClass "span" "userName" $
- R.text $
- fromMaybe "" . fmap _user_name $ CM.findUser (_exceedingPayer_userId payer) users
- R.elClass "span" "amount" $ do
- R.text "+ "
- R.text . Format.price currency $ _exceedingPayer_amount payer
-
-infos
- :: forall t m. MonadWidget t m
- => [User]
- -> Currency
- -> Map UserId Int
- -> Int
- -> m ()
-infos users currency repartition paymentCount =
- R.divClass "g-PaymentHeaderInfos__Repartition" $ do
-
- R.elClass "span" "total" $ do
- R.text $
- Msg.get $ Msg.Payment_Worth
- (T.intercalate " "
- [ (Format.number paymentCount)
- , if paymentCount > 1
- then Msg.get Msg.Payment_Many
- else Msg.get Msg.Payment_One
- ])
- (Format.price currency (M.foldl (+) 0 repartition))
-
- R.elClass "span" "partition" . R.text $
- let totalByUser =
- L.sortBy (\(_, t1) (_, t2) -> compare t2 t1)
- . M.toList
- $ repartition
- in T.intercalate ", " . flip map totalByUser $ \(userId, userTotal) ->
- Msg.get $ Msg.Payment_By
- (fromMaybe "" . fmap _user_name $ CM.findUser userId users)
- (Format.price currency userTotal)
diff --git a/client/src/View/Payment/Payment.hs b/client/src/View/Payment/Payment.hs
deleted file mode 100644
index 26444d7..0000000
--- a/client/src/View/Payment/Payment.hs
+++ /dev/null
@@ -1,101 +0,0 @@
-module View.Payment.Payment
- ( view
- , In(..)
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time.Clock (NominalDiffTime)
-import Prelude hiding (init)
-import Reflex.Dom (Dynamic, Event, MonadWidget, Reflex)
-import qualified Reflex.Dom as R
-
-import Common.Model (Currency, Frequency, Income (..),
- Payment (..), PaymentId,
- PaymentPage (..), User, UserId)
-import qualified Common.Util.Text as T
-
-import qualified Component.Pages as Pages
-import Loadable (Loadable (..))
-import qualified Loadable
-import qualified Util.Ajax as AjaxUtil
-import qualified Util.Reflex as ReflexUtil
-import qualified View.Payment.HeaderForm as HeaderForm
-import qualified View.Payment.HeaderInfos as HeaderInfos
-import qualified View.Payment.Reducer as Reducer
-import qualified View.Payment.Table as Table
-
-data In t = In
- { _in_currentUser :: UserId
- , _in_users :: [User]
- , _in_currency :: Currency
- }
-
-view :: forall t m. MonadWidget t m => In t -> m ()
-view input = do
-
- categories <- AjaxUtil.getNow "api/allCategories"
-
- R.dyn . R.ffor categories . Loadable.viewHideValueWhileLoading $ \categories -> do
-
- rec
- paymentPage <- Reducer.reducer $ Reducer.In
- { Reducer._in_page = page
- , Reducer._in_search = HeaderForm._out_search form
- , Reducer._in_frequency = HeaderForm._out_frequency form
- , Reducer._in_addPayment = addPayment
- , Reducer._in_editPayment = editPayment
- , Reducer._in_deletePayment = deletePayment
- }
-
- let eventFromResult :: forall a. ((Table.Out t, Pages.Out t) -> Event t a) -> m (Event t a)
- eventFromResult op = ReflexUtil.flatten . fmap (Maybe.fromMaybe R.never . fmap op) $ result
-
- let addPayment =
- R.leftmost
- [ tableAddPayment
- , HeaderForm._out_addPayment form
- ]
-
- page <- eventFromResult $ Pages._out_newPage . snd
- tableAddPayment <- eventFromResult $ Table._out_add . fst
- editPayment <- eventFromResult $ Table._out_edit . fst
- deletePayment <- eventFromResult $ Table._out_delete . fst
-
- form <- HeaderForm.view $ HeaderForm.In
- { HeaderForm._in_reset = () <$ addPayment
- , HeaderForm._in_categories = categories
- }
-
- result <- Loadable.viewShowValueWhileLoading paymentPage $
- \(PaymentPage page frequency header payments count) -> do
-
- HeaderInfos.view $ HeaderInfos.In
- { HeaderInfos._in_users = _in_users input
- , HeaderInfos._in_currency = _in_currency input
- , HeaderInfos._in_header = header
- , HeaderInfos._in_paymentCount = count
- }
-
- table <- Table.view $ Table.In
- { Table._in_users = _in_users input
- , Table._in_currentUser = _in_currentUser input
- , Table._in_categories = categories
- , Table._in_currency = _in_currency input
- , Table._in_payments = payments
- , Table._in_frequency = frequency
- }
-
- pages <- Pages.view $ Pages.In
- { Pages._in_total = R.constDyn count
- , Pages._in_perPage = Reducer.perPage
- , Pages._in_page = page
- }
-
- return (table, pages)
-
- return ()
-
- return ()
diff --git a/client/src/View/Payment/Reducer.hs b/client/src/View/Payment/Reducer.hs
deleted file mode 100644
index 3fe59b2..0000000
--- a/client/src/View/Payment/Reducer.hs
+++ /dev/null
@@ -1,110 +0,0 @@
-module View.Payment.Reducer
- ( perPage
- , reducer
- , In(..)
- , Params(..)
- ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time (NominalDiffTime)
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Frequency (..), PaymentPage)
-
-import Loadable (Loadable (..))
-import qualified Loadable as Loadable
-import qualified Util.Ajax as AjaxUtil
-import qualified Util.Either as EitherUtil
-
-perPage :: Int
-perPage = 7
-
-data In t a b c = In
- { _in_page :: Event t Int
- , _in_search :: Event t Text
- , _in_frequency :: Event t Frequency
- , _in_addPayment :: Event t a
- , _in_editPayment :: Event t b
- , _in_deletePayment :: Event t c
- }
-
-data Params = Params
- { _params_page :: Int
- , _params_search :: Text
- , _params_frequency :: Frequency
- } deriving (Show)
-
-initParams = Params 1 "" Punctual
-
-data Msg
- = Page Int
- | Search Text
- | Frequency Common.Model.Frequency
- | ResetSearch
- deriving Show
-
-reducer :: forall t m a b c. MonadWidget t m => In t a b c -> m (Dynamic t (Loadable PaymentPage))
-reducer input = do
-
- postBuild <- R.getPostBuild
-
- debouncedSearch <- R.debounce (1 :: NominalDiffTime) (_in_search input)
-
- params <- R.foldDynMaybe
- (\msg params -> case msg of
- Page page ->
- Just $ params { _params_page = page }
-
- Search "" ->
- if _params_search params == "" then
- Nothing
-
- else
- Just $ initParams { _params_frequency = _params_frequency params }
-
- Search search ->
- Just $ params { _params_search = search, _params_page = _params_page initParams }
-
- Frequency frequency ->
- Just $ params { _params_frequency = frequency, _params_page = _params_page initParams }
-
- ResetSearch ->
- Just $ initParams { _params_frequency = _params_frequency params }
- )
- initParams
- (R.leftmost
- [ Page <$> _in_page input
- , Search <$> debouncedSearch
- , Frequency <$> _in_frequency input
- , ResetSearch <$ _in_addPayment input
- ])
-
- let paramsEvent =
- R.leftmost
- [ initParams <$ postBuild
- , R.updated params
- , R.tag (R.current params) (_in_editPayment input)
- , R.tag (R.current params) (_in_deletePayment input)
- ]
-
- getResult <- AjaxUtil.get (pageUrl <$> paramsEvent)
-
- R.holdDyn
- Loading
- (R.leftmost
- [ Loading <$ paramsEvent
- , Loadable.fromEither <$> getResult
- ])
-
- where
- pageUrl (Params page search frequency) =
- "api/payments?page="
- <> (T.pack . show $ page)
- <> "&perPage="
- <> (T.pack . show $ perPage)
- <> "&search="
- <> search
- <> "&frequency="
- <> (T.pack $ show frequency)
diff --git a/client/src/View/Payment/Table.hs b/client/src/View/Payment/Table.hs
deleted file mode 100644
index 66065af..0000000
--- a/client/src/View/Payment/Table.hs
+++ /dev/null
@@ -1,145 +0,0 @@
-module View.Payment.Table
- ( view
- , In(..)
- , Out(..)
- ) where
-
-import qualified Data.List as L
-import qualified Data.Map as M
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import Reflex.Dom (Dynamic, Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Category (..), Currency,
- Frequency (..), Payment (..),
- User (..), UserId)
-import qualified Common.Model as CM
-import qualified Common.Msg as Msg
-import qualified Common.View.Format as Format
-
-import qualified Component.ConfirmDialog as ConfirmDialog
-import qualified Component.Table as Table
-import qualified Component.Tag as Tag
-import qualified Util.Ajax as Ajax
-import qualified Util.Either as EitherUtil
-import qualified View.Payment.Form as Form
-
-data In t = In
- { _in_users :: [User]
- , _in_currentUser :: UserId
- , _in_categories :: [Category]
- , _in_currency :: Currency
- , _in_payments :: [Payment]
- , _in_frequency :: Frequency
- }
-
-data Out t = Out
- { _out_add :: Event t ()
- , _out_edit :: Event t ()
- , _out_delete :: Event t ()
- }
-
-view :: forall t m. MonadWidget t m => In t -> m (Out t)
-view input = do
-
- table <- Table.view $ Table.In
- { Table._in_headerLabel = headerLabel (_in_frequency input)
- , Table._in_rows = _in_payments input
- , Table._in_cell =
- cell
- (_in_users input)
- (_in_categories input)
- (_in_frequency input)
- (_in_currency input)
- , Table._in_cloneModal = \payment ->
- Form.view $ Form.In
- { Form._in_categories = _in_categories input
- , Form._in_operation = Form.Clone payment
- , Form._in_frequency = _in_frequency input
- }
- , Table._in_editModal = \payment ->
- Form.view $ Form.In
- { Form._in_categories = _in_categories input
- , Form._in_operation = Form.Edit payment
- , Form._in_frequency = _in_frequency input
- }
- , Table._in_deleteModal = \payment ->
- ConfirmDialog.view $ ConfirmDialog.In
- { ConfirmDialog._in_header = Msg.get Msg.Payment_DeleteConfirm
- , ConfirmDialog._in_confirm = \e -> do
- res <- Ajax.delete
- (R.constDyn $ T.concat ["/api/payment/", T.pack . show $ _payment_id payment])
- e
- return $ () <$ R.fmapMaybe EitherUtil.eitherToMaybe res
- }
- , Table._in_canEdit = (== (_in_currentUser input)) . _payment_user
- , Table._in_canDelete = (== (_in_currentUser input)) . _payment_user
- }
-
- return $ Out
- { _out_add = Table._out_add table
- , _out_edit = Table._out_edit table
- , _out_delete = Table._out_delete table
- }
-
-data Header
- = NameHeader
- | CostHeader
- | UserHeader
- | CategoryHeader
- | DateHeader
- deriving (Eq, Show, Bounded, Enum)
-
-headerLabel :: Frequency -> Header -> Text
-headerLabel _ NameHeader = Msg.get Msg.Payment_Name
-headerLabel _ CostHeader = Msg.get Msg.Payment_Cost
-headerLabel _ UserHeader = Msg.get Msg.Payment_User
-headerLabel _ CategoryHeader = Msg.get Msg.Payment_Category
-headerLabel Punctual DateHeader = Msg.get Msg.Payment_Date
-headerLabel Monthly DateHeader = ""
-
-cell
- :: forall t m. MonadWidget t m
- => [User]
- -> [Category]
- -> Frequency
- -> Currency
- -> Header
- -> Payment
- -> m ()
-cell users categories frequency currency header payment =
- case header of
- NameHeader ->
- R.text $ _payment_name payment
-
- CostHeader ->
- R.divClass (if amount < 0 then "g-Payment__Refund" else "") $
- R.text $ Format.price currency amount
- where amount = _payment_cost payment
-
- UserHeader ->
- R.text . Maybe.fromMaybe "" . fmap _user_name $ CM.findUser (_payment_user payment) users
-
- CategoryHeader ->
- let
- category =
- L.find ((== (_payment_category payment)) . _category_id) categories
- in
- Maybe.fromMaybe R.blank . flip fmap category $ \c ->
- Tag.view $ Tag.In
- { Tag._in_text = _category_name c
- , Tag._in_color = _category_color c
- }
-
- DateHeader ->
- if frequency == Punctual then
- do
- R.elClass "span" "shortDate" $
- R.text . Format.shortDay . _payment_date $ payment
-
- R.elClass "span" "longDate" $
- R.text . Format.longDay . _payment_date $ payment
- else
- R.blank
diff --git a/client/src/View/SignIn.hs b/client/src/View/SignIn.hs
deleted file mode 100644
index e68755f..0000000
--- a/client/src/View/SignIn.hs
+++ /dev/null
@@ -1,82 +0,0 @@
-module View.SignIn
- ( view
- , Out(..)
- ) where
-
-import qualified Data.Either as Either
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Validation as V
-import Reflex.Dom (Event, MonadWidget)
-import qualified Reflex.Dom as R
-
-import Common.Model (Init, SignInForm (SignInForm))
-import qualified Common.Msg as Msg
-import qualified Common.Validation.SignIn as SignInValidation
-
-import qualified Component.Button as Button
-import qualified Component.Form as Form
-import qualified Component.Input as Input
-import qualified Util.Ajax as Ajax
-import qualified Util.Validation as ValidationUtil
-import qualified Util.WaitFor as WaitFor
-
-data Out t = Out
- { _out_success :: Event t Init
- }
-
-view :: forall t m. MonadWidget t m => m (Out t)
-view = do
- signInResult <- R.divClass "signIn" $
- Form.view $ do
- rec
- let resetForm = ("" <$ R.ffilter Either.isRight signInResult)
-
- email <- Input._out_raw <$> (Input.view
- (Input.defaultIn
- { Input._in_label = Msg.get Msg.SignIn_EmailLabel
- , Input._in_validation = SignInValidation.email
- })
- resetForm
- validate)
-
- password <- Input._out_raw <$> (Input.view
- (Input.defaultIn
- { Input._in_label = Msg.get Msg.SignIn_PasswordLabel
- , Input._in_validation = SignInValidation.password
- , Input._in_inputType = "password"
- })
- resetForm
- validate)
-
- validate <- Button._out_clic <$> (Button.view $
- (Button.defaultIn (R.text $ Msg.get Msg.SignIn_Button))
- { Button._in_class = R.constDyn "validate"
- , Button._in_waiting = waiting
- , Button._in_submit = True
- })
-
- let form = do
- e <- email
- p <- password
- return . V.Success $ SignInForm e p
-
- (signInResult, waiting) <- WaitFor.waitFor
- (Ajax.postAndParseResult "/api/signIn")
- (ValidationUtil.fireValidation form validate)
-
- showSignInResult signInResult
-
- return signInResult
-
- return $ Out
- { _out_success = R.filterRight signInResult
- }
-
-showSignInResult :: forall t m. MonadWidget t m => Event t (Either Text Init) -> m ()
-showSignInResult signInResult = do
- _ <- R.widgetHold R.blank $ showResult <$> signInResult
- R.blank
-
- where showResult (Left error) = R.divClass "error" . R.text $ error
- showResult (Right _) = R.blank
diff --git a/client/src/View/Statistics/Chart.hs b/client/src/View/Statistics/Chart.hs
deleted file mode 100644
index 63df2a1..0000000
--- a/client/src/View/Statistics/Chart.hs
+++ /dev/null
@@ -1,102 +0,0 @@
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE JavaScriptFFI #-}
-
-module View.Statistics.Chart
- ( view
- , In(..)
- , Dataset(..)
- ) where
-
-import qualified Control.Concurrent as Concurrent
-import Control.Monad (void)
-import Control.Monad.IO.Class (liftIO)
-import Data.Aeson ((.=))
-import qualified Data.Aeson as AE
-import qualified Data.Map as M
-import Data.Text (Text)
-import Language.Javascript.JSaddle (JSString, JSVal)
-import qualified Language.Javascript.JSaddle.Value as JSValue
-import Reflex.Dom (MonadWidget)
-import qualified Reflex.Dom as R
--- import GHCJS.Foreign.Callback
-
-
-#ifdef __GHCJS__
-foreign import javascript unsafe "new Chart(document.getElementById($1), $2);" drawChart :: JSString -> JSVal -> IO ()
-#else
-drawChart = error "drawChart: only available from JavaScript"
-#endif
-
-data In = In
- { _in_title :: Text
- , _in_labels :: [Text]
- , _in_datasets :: [Dataset]
- }
-
-data Dataset = Dataset
- { _dataset_label :: Text
- , _dataset_data :: [Int]
- , _dataset_color :: Text
- }
-
-view :: forall t m. MonadWidget t m => In -> m ()
-view input = do
- R.divClass "g-Chart" $
- R.elAttr "canvas" (M.singleton "id" "chart") $
- R.blank
-
- liftIO $ Concurrent.forkIO $ do
- Concurrent.threadDelay 500000
- config <- JSValue.valMakeJSON (configToJson input)
- drawChart "chart" config
-
- return ()
-
-configToJson (In title labels datasets) =
- AE.object
- [ "type" .= AE.String "line"
- , "data" .=
- AE.object
- [ "labels" .= labels
- , "datasets" .= map datasetToJson datasets
- ]
- , "options" .=
- AE.object
- [ "responsive" .= True
- , "title" .=
- AE.object
- [ "display" .= True
- , "text" .= title
- ]
- , "tooltips" .=
- AE.object
- [ "mode" .= AE.String "nearest"
- , "intersect" .= False
- ]
- , "hover" .=
- AE.object
- [ "mode" .= AE.String "nearest"
- , "intersect" .= True
- ]
- , "scales" .=
- AE.object
- [ "yAxes" .=
- [ [ AE.object
- [ "ticks" .=
- AE.object
- [ "beginAtZero" .= True ]
- ]
- ]
- ]
- ]
- ]
- ]
-
-datasetToJson (Dataset label data_ color) =
- AE.object
- [ "label" .= label
- , "data" .= data_
- , "fill" .= False
- , "backgroundColor" .= color
- , "borderColor" .= color
- ]
diff --git a/client/src/View/Statistics/Statistics.hs b/client/src/View/Statistics/Statistics.hs
deleted file mode 100644
index d931b2b..0000000
--- a/client/src/View/Statistics/Statistics.hs
+++ /dev/null
@@ -1,85 +0,0 @@
-module View.Statistics.Statistics
- ( view
- , In(..)
- ) where
-
-import Control.Monad (void)
-import Data.Map (Map)
-import qualified Data.Map as M
-import qualified Data.Text as T
-import Data.Time.Calendar (Day)
-import qualified Data.Time.Calendar as Calendar
-import Loadable (Loadable)
-import qualified Loadable
-import Reflex.Dom (Dynamic, MonadWidget)
-import qualified Reflex.Dom as R
-import qualified Util.Ajax as AjaxUtil
-import qualified View.Statistics.Chart as Chart
-
-import Common.Model (Category (..), Currency, Income,
- MonthStats (..), Stats, User (..))
-import qualified Common.Msg as Msg
-import qualified Common.View.Format as Format
-
-data In = In
- { _in_currency :: Currency
- }
-
-view :: forall t m. MonadWidget t m => In -> m ()
-view input = do
-
- users <- AjaxUtil.getNow "api/users"
- categories <- AjaxUtil.getNow "api/allCategories"
- statistics <- AjaxUtil.getNow "api/statistics"
-
- let loadable = (\u c s -> (,,) <$> u <*> c <*> s) <$> users <*> categories <*> statistics
-
- R.divClass "withMargin" $
- R.divClass "titleButton" $
- R.el "h1" $
- R.text $ Msg.get Msg.Statistics_Title
-
- void . R.dyn . R.ffor loadable . Loadable.viewHideValueWhileLoading $
- stats (_in_currency input)
-
-stats :: forall t m. MonadWidget t m => Currency -> ([User], [Category], Stats) -> m ()
-stats currency (users, categories, stats) =
- Chart.view $ Chart.In
- { Chart._in_title = Msg.get (Msg.Statistics_ByMonthsAndMean averagePayment averageIncome)
- , Chart._in_labels = map (Format.monthAndYear . _monthStats_start) stats
- , Chart._in_datasets = totalIncomeDataset : totalPaymentDataset : (map categoryDataset categories)
- }
-
- where
- averageIncome =
- Format.price currency $ sum totalIncomes `div` length stats
-
- totalIncomeDataset =
- Chart.Dataset
- { Chart._dataset_label = Msg.get Msg.Statistics_TotalIncomes
- , Chart._dataset_data = totalIncomes
- , Chart._dataset_color = "#222222"
- }
-
- totalIncomes =
- map (sum . map snd . M.toList . _monthStats_incomeByUser) stats
-
- averagePayment =
- Format.price currency $ sum totalPayments `div` length stats
-
- totalPaymentDataset =
- Chart.Dataset
- { Chart._dataset_label = Msg.get Msg.Statistics_TotalPayments
- , Chart._dataset_data = totalPayments
- , Chart._dataset_color = "#555555"
- }
-
- totalPayments =
- map (sum . map snd . M.toList . _monthStats_paymentsByCategory) stats
-
- categoryDataset category =
- Chart.Dataset
- { Chart._dataset_label = _category_name category
- , Chart._dataset_data = map (M.findWithDefault 0 (_category_id category) . _monthStats_paymentsByCategory) stats
- , Chart._dataset_color = _category_color category
- }
diff --git a/common/LICENSE b/common/LICENSE
deleted file mode 100644
index 45644ff..0000000
--- a/common/LICENSE
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- <one line to give the program's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- <program> Copyright (C) <year> <name of author>
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<http://www.gnu.org/licenses/>.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/common/Setup.hs b/common/Setup.hs
deleted file mode 100644
index 4467109..0000000
--- a/common/Setup.hs
+++ /dev/null
@@ -1,2 +0,0 @@
-import Distribution.Simple
-main = defaultMain
diff --git a/common/common.cabal b/common/common.cabal
deleted file mode 100644
index dffc8d0..0000000
--- a/common/common.cabal
+++ /dev/null
@@ -1,72 +0,0 @@
-Name: common
-Version: 0.0.1
-License: GPL-3
-License-file: LICENSE
-Author: Joris Guyonvarch
-Maintainer: joris@guyonvarch.me
-Category: Web
-Build-type: Simple
-Cabal-version: >=1.10
-
-Library
- Ghc-options: -Wall -Werror
- Hs-source-dirs: src
- Default-language: Haskell2010
-
- Default-extensions:
- DeriveGeneric
- ExistentialQuantification
- LambdaCase
- MultiParamTypeClasses
- OverloadedStrings
- ScopedTypeVariables
-
- Build-depends:
- aeson
- , base >= 4.11 && < 5
- , containers
- , text
- , time
- , validation
-
- Exposed-modules:
- Common.Model
- Common.Model.CreateCategoryForm
- Common.Model.CreateIncomeForm
- Common.Model.CreatePaymentForm
- Common.Model.Email
- Common.Model.Password
- Common.Model.Payment
- Common.Model.SignInForm
- Common.Model.User
- Common.Msg
- Common.Util.Text
- Common.Util.Time
- Common.Util.Validation
- Common.Validation.Atomic
- Common.Validation.Category
- Common.Validation.Income
- Common.Validation.Payment
- Common.Validation.SignIn
- Common.View.Format
-
- other-modules:
- Common.Message.Key
- Common.Message.Lang
- Common.Message.Translation
- Common.Model.Category
- Common.Model.CategoryPage
- Common.Model.Currency
- Common.Model.EditCategoryForm
- Common.Model.EditIncome
- Common.Model.EditIncomeForm
- Common.Model.EditPaymentForm
- Common.Model.ExceedingPayer
- Common.Model.Frequency
- Common.Model.Income
- Common.Model.IncomeHeader
- Common.Model.IncomePage
- Common.Model.Init
- Common.Model.PaymentHeader
- Common.Model.PaymentPage
- Common.Model.Stats
diff --git a/common/src/Common/Message/Key.hs b/common/src/Common/Message/Key.hs
deleted file mode 100644
index f3b0837..0000000
--- a/common/src/Common/Message/Key.hs
+++ /dev/null
@@ -1,150 +0,0 @@
-module Common.Message.Key
- ( Key(..)
- ) where
-
-import Data.Text
-
-data Key =
-
- App_Title
-
- | Category_Add
- | Category_Clone
- | Category_Color
- | Category_DeleteConfirm
- | Category_Edit
- | Category_Empty
- | Category_Name
- | Category_NotDeleted
- | Category_Title
- | Category_Used
-
- | Date_Long Int Text Int
- | Date_Short Int Int Int
- | Date_ShortMonthAndYear Int Int
-
- | Dialog_Confirm
- | Dialog_Undo
-
- | Error_CategoryCreate
- | Error_CategoryDelete
- | Error_CategoryEdit
- | Error_IncomeCreate
- | Error_IncomeDelete
- | Error_IncomeEdit
- | Error_PaymentCreate
- | Error_PaymentDelete
- | Error_PaymentEdit
- | Error_SignOut
-
- | Form_AlreadyExists
- | Form_NonEmpty
- | Form_MinChars Int
- | Form_NonNullNumber
- | Form_GreaterIntThan Int
- | Form_InvalidCategory
- | Form_InvalidColor
- | Form_InvalidDate
- | Form_InvalidInt
- | Form_InvalidString
- | Form_SmallerIntThan Int
-
- | HttpError_BadPayload
- | HttpError_BadUrl
- | HttpError_NetworkError
- | HttpError_Timeout
-
- | Income_AddLong
- | Income_AddShort
- | Income_Name
- | Income_Amount
- | Income_Clone
- | Income_CumulativeSince Text
- | Income_Date
- | Income_DeleteConfirm
- | Income_Edit
- | Income_Empty
- | Income_MonthlyNet
- | Income_Title
-
- | Month_January
- | Month_February
- | Month_March
- | Month_April
- | Month_May
- | Month_June
- | Month_July
- | Month_August
- | Month_September
- | Month_October
- | Month_November
- | Month_December
-
- | PageNotFound_Title
-
- | Payment_Add
- | Payment_Balanced
- | Payment_By Text Text
- | Payment_Category
- | Payment_CloneLong
- | Payment_CloneShort
- | Payment_Cost
- | Payment_Date
- | Payment_Delete
- | Payment_DeleteConfirm
- | Payment_EditLong
- | Payment_EditShort
- | Payment_Empty
- | Payment_Frequency
- | Payment_InvalidFrequency
- | Payment_Many
- | Payment_MonthlyFemale
- | Payment_MonthlyMale
- | Payment_Name
- | Payment_NotDeleted
- | Payment_One
- | Payment_PunctualFemale
- | Payment_PunctualMale
- | Payment_Title
- | Payment_User
- | Payment_Worth Text Text
-
- | Search_Monthly
- | Search_Name
- | Search_Punctual
-
- | Secure_Forbidden
- | Secure_Unauthorized
-
- | SignIn_Button
- | SignIn_DisconnectSuccess
- | SignIn_InvalidCredentials
- | SignIn_EmailLabel
- | SignIn_PasswordLabel
-
- | Statistics_Title
- | Statistics_ByMonthsAndMean Text Text
- | Statistics_TotalPayments
- | Statistics_TotalIncomes
-
- | WeeklyReport_Empty
- | WeeklyReport_IncomesCreated Int
- | WeeklyReport_IncomesDeleted Int
- | WeeklyReport_IncomesEdited Int
- | WeeklyReport_IncomeCreated Int
- | WeeklyReport_IncomeDeleted Int
- | WeeklyReport_IncomeEdited Int
- | WeeklyReport_PayedFor Text Text Text Text
- | WeeklyReport_PayedForNot Text Text Text Text
- | WeeklyReport_PayedFrom Text Text Text
- | WeeklyReport_PayedFromNot Text Text Text
- | WeeklyReport_PaymentsCreated Int
- | WeeklyReport_PaymentsDeleted Int
- | WeeklyReport_PaymentsEdited Int
- | WeeklyReport_PaymentCreated Int
- | WeeklyReport_PaymentDeleted Int
- | WeeklyReport_PaymentEdited Int
- | WeeklyReport_Title
-
- | NotFound_Message
- | NotFound_LinkMessage
diff --git a/common/src/Common/Message/Lang.hs b/common/src/Common/Message/Lang.hs
deleted file mode 100644
index 0a32ede..0000000
--- a/common/src/Common/Message/Lang.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Common.Message.Lang
- ( Lang(..)
- ) where
-
-data Lang =
- English
- | French
diff --git a/common/src/Common/Message/Translation.hs b/common/src/Common/Message/Translation.hs
deleted file mode 100644
index f1de3e1..0000000
--- a/common/src/Common/Message/Translation.hs
+++ /dev/null
@@ -1,655 +0,0 @@
-module Common.Message.Translation
- ( get
- ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-
-import Common.Message.Key
-import Common.Message.Lang (Lang (..))
-
-get :: Lang -> Key -> Text
-get = m
-
-m :: Lang -> Key -> Text
-
-m l App_Title =
- case l of
- English -> "Shared Cost"
- French -> "Partage des frais"
-
-m l Category_Add =
- case l of
- English -> "Add an category"
- French -> "Ajouter une catégorie"
-
-m l Category_Clone =
- case l of
- English -> "Clone an category"
- French -> "Cloner une catégorie"
-
-m l Category_Color =
- case l of
- English -> "Color"
- French -> "Couleur"
-
-m l Category_DeleteConfirm =
- case l of
- English -> "Are you sure to delete this category ?"
- French -> "Voulez-vous vraiment supprimer cette catégorie ?"
-
-m l Category_Edit =
- case l of
- English -> "Edit an category"
- French -> "Modifier une catégorie"
-
-m l Category_Empty =
- case l of
- English -> "No category."
- French -> "Aucune catégorie."
-
-m l Category_Name =
- case l of
- English -> "Name"
- French -> "Nom"
-
-m l Category_NotDeleted =
- case l of
- English -> "The category could not have been deleted."
- French -> "La catégorie n’a pas pu être supprimé."
-
-m l Category_Title =
- case l of
- English -> "Categories"
- French -> "Catégories"
-
-m l Category_Used =
- case l of
- English -> "This category is currently being used"
- French -> "Cette catégorie est actuellement utilisée"
-
-m l (Date_Short day month year) =
- case l of
- English ->
- T.intercalate "-" [ padded year 4, padded month 2, padded day 2 ]
- French ->
- T.intercalate "/" [ padded day 2, padded month 2, padded year 4 ]
- where padded num pad =
- let str = show num
- in T.pack $ replicate (pad - length str) '0' ++ str
-
-m l (Date_ShortMonthAndYear month year) =
- case l of
- English ->
- T.intercalate "-" . map (T.pack . show) $ [ year, month ]
- French ->
- T.intercalate "/" . map (T.pack . show) $ [ month, year ]
-
-m l (Date_Long day month year) =
- case l of
- English ->
- T.concat [ month, " " , T.pack . show $ day, ", ", T.pack . show $ year ]
- French ->
- T.intercalate " " [ T.pack . show $ day, month, T.pack . show $ year ]
-
-m l Dialog_Confirm =
- case l of
- English -> "Confirm"
- French -> "Confirmer"
-
-m l Dialog_Undo =
- case l of
- English -> "Undo"
- French -> "Annuler"
-
-m l Error_CategoryCreate =
- case l of
- English -> "Error at category creation"
- French -> "Erreur lors de la création de la catégorie"
-
-m l Error_CategoryDelete =
- case l of
- English -> "Error at category deletion"
- French -> "Erreur lors de la suppression de la catégorie"
-
-m l Error_CategoryEdit =
- case l of
- English -> "Error at category edition"
- French -> "Erreur lors de la modification de la catégorie"
-
-m l Error_IncomeCreate =
- case l of
- English -> "Error at income creation"
- French -> "Erreur lors de la création du revenu"
-
-m l Error_IncomeDelete =
- case l of
- English -> "Error at income deletion"
- French -> "Erreur lors de la suppression du revenu"
-
-m l Error_IncomeEdit =
- case l of
- English -> "Error at income edition"
- French -> "Erreur lors de la modification du revenu"
-
-m l Error_PaymentCreate =
- case l of
- English -> "Error at payment creation"
- French -> "Erreur lors de la création du paiement"
-
-m l Error_PaymentDelete =
- case l of
- English -> "Error at payment deletion"
- French -> "Erreur lors de la suppression du paiement"
-
-m l Error_PaymentEdit =
- case l of
- English -> "Error at payment edition"
- French -> "Erreur lors de la modification du paiement"
-
-m l Error_SignOut =
- case l of
- English -> "Error at sign out"
- French -> "Erreur lors de la déconnexion"
-
-m l Form_AlreadyExists =
- case l of
- English -> "Dupplicate field"
- French -> "Doublon"
-
-m l Form_NonEmpty =
- case l of
- English -> "Required field"
- French -> "Champ requis"
-
-m l (Form_MinChars number) =
- case l of
- English -> T.concat [ "This field must contains at least ", T.pack . show $ number, " characters" ]
- French -> T.concat [ "Ce champ doit contenir au moins ", T.pack . show $ number, " caractères" ]
-
-m l Form_NonNullNumber =
- case l of
- English -> "Number must not be null"
- French -> "Le nombre ne doit pas être nul"
-
-m l (Form_GreaterIntThan number) =
- case l of
- English -> T.concat [ "Integer smaller than ", T.pack . show $ number, " or equal required" ]
- French -> T.concat [ "Entier inférieur ou égal à ", T.pack . show $ number, " requis" ]
-
-m l Form_InvalidCategory =
- case l of
- English -> "Invalid category"
- French -> "Catégorie invalide"
-
-m l Form_InvalidColor =
- case l of
- English -> "Invalid color"
- French -> "Couleur invalide"
-
-m l Form_InvalidDate =
- case l of
- English -> "Date required"
- French -> "Date requise"
-
-m l Form_InvalidInt =
- case l of
- English -> "Integer required"
- French -> "Entier requis"
-
-m l Form_InvalidString =
- case l of
- English -> "String required"
- French -> "Chaîne de caractères requise"
-
-m l (Form_SmallerIntThan number) =
- case l of
- English -> T.concat [ "Integer bigger than ", T.pack . show $ number, " or equal required" ]
- French -> T.concat [ "Entier supérieur ou égal à ", T.pack . show $ number, " requis" ]
-
-m l HttpError_BadPayload =
- case l of
- English -> "Bad payload server error"
- French -> "Contenu inattendu en provenance du serveur"
-
-m l HttpError_BadUrl =
- case l of
- English -> "URL not valid"
- French -> "l’URL n’est pas valide"
-
-m l HttpError_NetworkError =
- case l of
- English -> "Network can not be reached"
- French -> "Le serveur n’est pas accessible"
-
-m l HttpError_Timeout =
- case l of
- English -> "Timeout server error"
- French -> "Le serveur met trop de temps à répondre"
-
-m l Income_AddLong =
- case l of
- English -> "Add an income"
- French -> "Ajouter un revenu"
-
-m l Income_AddShort =
- case l of
- English -> "Add"
- French -> "Ajouter"
-
-m l Income_Name =
- case l of
- English -> "Name"
- French -> "Nom"
-
-m l Income_Amount =
- case l of
- English -> "Income"
- French -> "Revenu"
-
-m l Income_Clone =
- case l of
- English -> "Clone an income"
- French -> "Cloner un revenu"
-
-m l (Income_CumulativeSince since) =
- case l of
- English -> T.concat [ "Cumulative incomes since ", since ]
- French -> T.concat [ "Revenus nets cumulés depuis le ", since ]
-
-m l Income_Date =
- case l of
- English -> "Date"
- French -> "Date"
-
-m l Income_DeleteConfirm =
- case l of
- English -> "Are you sure to delete this income ?"
- French -> "Voulez-vous vraiment supprimer ce revenu ?"
-
-m l Income_Edit =
- case l of
- English -> "Edit an income"
- French -> "Modifier un revenu"
-
-m l Income_Empty =
- case l of
- English -> "No income."
- French -> "Aucun revenu."
-
-m l Income_MonthlyNet =
- case l of
- English -> "Net monthly incomes"
- French -> "Revenus mensuels nets"
-
-m l Income_Title =
- case l of
- English -> "Income"
- French -> "Revenu"
-
-m l Month_January =
- case l of
- English -> "january"
- French -> "janvier"
-
-m l Month_February =
- case l of
- English -> "february"
- French -> "février"
-
-m l Month_March =
- case l of
- English -> "march"
- French -> "mars"
-
-m l Month_April =
- case l of
- English -> "april"
- French -> "avril"
-
-m l Month_May =
- case l of
- English -> "may"
- French -> "mai"
-
-m l Month_June =
- case l of
- English -> "june"
- French -> "juin"
-
-m l Month_July =
- case l of
- English -> "july"
- French -> "juillet"
-
-m l Month_August =
- case l of
- English -> "august"
- French -> "août"
-
-m l Month_September =
- case l of
- English -> "september"
- French -> "septembre"
-
-m l Month_October =
- case l of
- English -> "october"
- French -> "octobre"
-
-m l Month_November =
- case l of
- English -> "november"
- French -> "novembre"
-
-m l Month_December =
- case l of
- English -> "december"
- French -> "décembre"
-
-m l PageNotFound_Title =
- case l of
- English -> "Page not found"
- French -> "Page introuvable"
-
-m l Payment_Add =
- case l of
- English -> "Add a payment"
- French -> "Ajouter un paiement"
-
-m l Payment_Balanced =
- case l of
- English -> "Payments are balanced."
- French -> "Les paiements sont équilibrés."
-
-m l (Payment_By key value) =
- case l of
- English -> T.concat [ key, ": ", value ]
- French -> T.concat [ key, " : ", value ]
-
-m l Payment_Category =
- case l of
- English -> "Category"
- French -> "Catégorie"
-
-m l Payment_CloneLong =
- case l of
- English -> "Clone a payment"
- French -> "Cloner un paiement"
-
-m l Payment_CloneShort =
- case l of
- English -> "Clone"
- French -> "Cloner"
-
-m l Payment_Cost =
- case l of
- English -> "Cost"
- French -> "Coût"
-
-m l Payment_Date =
- case l of
- English -> "Date"
- French -> "Date"
-
-m l Payment_Delete =
- case l of
- English -> "Delete"
- French -> "Supprimer"
-
-m l Payment_DeleteConfirm =
- case l of
- English -> "Are you sure to delete this payment ?"
- French -> "Voulez-vous vraiment supprimer ce paiement ?"
-
-m l Payment_EditLong =
- case l of
- English -> "Edit a payment"
- French -> "Modifier un paiement"
-
-m l Payment_EditShort =
- case l of
- English -> "Edit"
- French -> "Modifier"
-
-m l Payment_Empty =
- case l of
- English -> "No payment found from your search criteria."
- French -> "Aucun paiement ne correspond à vos critères de recherches."
-
-m l Payment_Frequency =
- case l of
- English -> "Frequency"
- French -> "Fréquence"
-
-m l Payment_InvalidFrequency =
- case l of
- English -> "Invalid frequency"
- French -> "Fréquence invalide"
-
-m l Payment_Many =
- case l of
- English -> "payments"
- French -> "paiements"
-
-m l Payment_MonthlyFemale =
- case l of
- English -> "Monthly"
- French -> "Mensuelle"
-
-m l Payment_MonthlyMale =
- case l of
- English -> "Monthly"
- French -> "Mensuel"
-
-m l Payment_Name =
- case l of
- English -> "Name"
- French -> "Nom"
-
-m l Payment_NotDeleted =
- case l of
- English -> "The payment could not have been deleted."
- French -> "Le paiement n’a pas pu être supprimé."
-
-m l Payment_One =
- case l of
- English -> "payment"
- French -> "paiement"
-
-m l Payment_PunctualFemale =
- case l of
- English -> "Punctual"
- French -> "Ponctuelle"
-
-m l Payment_PunctualMale =
- case l of
- English -> "Punctual"
- French -> "Ponctuel"
-
-m l Payment_Title =
- case l of
- English -> "Payments"
- French -> "Paiements"
-
-m l Payment_User =
- case l of
- English -> "Payer"
- French -> "Payeur"
-
-m l (Payment_Worth subject amount) =
- case l of
- English -> T.concat [ subject, " worth ", amount ]
- French -> T.concat [ subject, " comptabilisant ", amount ]
-
-m l Search_Monthly =
- case l of
- English -> "Monthly"
- French -> "Mensuel"
-
-m l Search_Name =
- case l of
- English -> "Search"
- French -> "Recherche"
-
-m l Search_Punctual =
- case l of
- English -> "Punctual"
- French -> "Ponctuel"
-
-m l Secure_Unauthorized =
- case l of
- English -> "You are not authorized to sign in."
- French -> "Tu n’es pas autorisé à te connecter."
-
-m l Secure_Forbidden =
- case l of
- English -> "You need to be logged in to perform this action"
- French -> "Tu dois te connecter pour effectuer cette action"
-
-m l SignIn_Button =
- case l of
- English -> "Sign in"
- French -> "Connexion"
-
-m l SignIn_DisconnectSuccess =
- case l of
- English -> "You have successfully disconnected"
- French -> "Vous êtes à présent déconnecté."
-
-m l SignIn_InvalidCredentials =
- case l of
- English -> "Your credentials are not valid."
- French -> "Vos identifiants de connexion ne sont pas valides."
-
-m l SignIn_EmailLabel =
- case l of
- English -> "Email"
- French -> "Courriel"
-
-m l SignIn_PasswordLabel =
- case l of
- English -> "Password"
- French -> "Mot de passe"
-
-m l (Statistics_ByMonthsAndMean paymentMean incomeMean ) =
- case l of
- English ->
- T.concat [ "Payments by category (mean ", paymentMean, ") and income (mean ", incomeMean, ") by month" ]
- French ->
- T.concat [ "Paiements par catégorie (moy. ", paymentMean, ") et revenu (moy. ", incomeMean, ") par mois" ]
-
-m l Statistics_Title =
- case l of
- English -> "Statistics"
- French -> "Statistiques"
-
-m l Statistics_TotalPayments =
- case l of
- English -> "Payment total"
- French -> "Total des paiements"
-
-m l Statistics_TotalIncomes =
- case l of
- English -> "Income total"
- French -> "Total des revenus"
-
-m l WeeklyReport_Empty =
- case l of
- English -> "No activity the previous week."
- French -> "Pas d’activité la semaine passée."
-
-m l (WeeklyReport_IncomesCreated count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " incomes created:" ]
- French -> T.concat [ T.pack . show $ count, " revenus créés :" ]
-
-m l (WeeklyReport_IncomesDeleted count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " incomes deleted:" ]
- French -> T.concat [ T.pack . show $ count, " revenus supprimés :" ]
-
-m l (WeeklyReport_IncomesEdited count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " incomes edited:" ]
- French -> T.concat [ T.pack . show $ count, " revenus modifiés :" ]
-
-m l (WeeklyReport_IncomeCreated count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " income created:" ]
- French -> T.concat [ T.pack . show $ count, " revenu créé :" ]
-
-m l (WeeklyReport_IncomeDeleted count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " income deleted:" ]
- French -> T.concat [ T.pack . show $ count, " revenu supprimé :" ]
-
-m l (WeeklyReport_IncomeEdited count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " income edited:" ]
- French -> T.concat [ T.pack . show $ count, " revenu modifié :" ]
-
-m l (WeeklyReport_PayedFor name amount for at) =
- case l of
- English -> T.concat [ name, " payed ", amount, " for “", for, "” at ", at ]
- French -> T.concat [ name, " a payé ", amount, " de « ", for, " » le ", at ]
-
-m l (WeeklyReport_PayedForNot name amount for at) =
- case l of
- English -> T.concat [ name, " didn’t pay ", amount, " for “", for, "” at ", at ]
- French -> T.concat [ name, " n’a pas payé ", amount, " de « ", for, " » le ", at ]
-
-m l (WeeklyReport_PayedFrom name amount for) =
- case l of
- English -> T.concat [ name, " is payed ", amount, " of net monthly income from ", for ]
- French -> T.concat [ name, " est payé ", amount, " net par mois à partir du ", for ]
-
-m l (WeeklyReport_PayedFromNot name amount for) =
- case l of
- English -> T.concat [ name, " isn’t payed ", amount, " of net monthly income from ", for ]
- French -> T.concat [ name, " n’est pas payé ", amount, " net par mois à partir du ", for ]
-
-m l (WeeklyReport_PaymentsCreated count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " payments created:" ]
- French -> T.concat [ T.pack . show $ count, " paiements créés :" ]
-
-m l (WeeklyReport_PaymentsDeleted count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " payments deleted:" ]
- French -> T.concat [ T.pack . show $ count, " paiements supprimés :" ]
-
-m l (WeeklyReport_PaymentsEdited count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " payments edited:" ]
- French -> T.concat [ T.pack . show $ count, " paiements modifiés :" ]
-
-m l (WeeklyReport_PaymentCreated count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " payment created:" ]
- French -> T.concat [ T.pack . show $ count, " paiement créé :" ]
-
-m l (WeeklyReport_PaymentDeleted count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " payment deleted:" ]
- French -> T.concat [ T.pack . show $ count, " paiement supprimé :" ]
-
-m l (WeeklyReport_PaymentEdited count) =
- case l of
- English -> T.concat [ T.pack . show $ count, " payment edited:" ]
- French -> T.concat [ T.pack . show $ count, " paiement modifié :" ]
-
-m l WeeklyReport_Title =
- case l of
- English -> "Weekly report"
- French -> "Rapport hebdomadaire"
-
-m l NotFound_Message =
- case l of
- English -> "There is nothing here!"
- French -> "Il n’y a rien à voir ici."
-
-m l NotFound_LinkMessage =
- case l of
- English -> "Go back to the home page."
- French -> "Retour à l’accueil."
diff --git a/common/src/Common/Model.hs b/common/src/Common/Model.hs
deleted file mode 100644
index 979d876..0000000
--- a/common/src/Common/Model.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Common.Model (module X) where
-
-import Common.Model.Category as X
-import Common.Model.CategoryPage as X
-import Common.Model.CreateCategoryForm as X
-import Common.Model.CreateIncomeForm as X
-import Common.Model.CreatePaymentForm as X
-import Common.Model.Currency as X
-import Common.Model.EditCategoryForm as X
-import Common.Model.EditIncome as X
-import Common.Model.EditIncomeForm as X
-import Common.Model.EditPaymentForm as X
-import Common.Model.Email as X
-import Common.Model.ExceedingPayer as X
-import Common.Model.Frequency as X
-import Common.Model.Income as X
-import Common.Model.IncomeHeader as X
-import Common.Model.IncomePage as X
-import Common.Model.Init as X
-import Common.Model.Password as X
-import Common.Model.Payment as X
-import Common.Model.PaymentHeader as X
-import Common.Model.PaymentPage as X
-import Common.Model.SignInForm as X
-import Common.Model.Stats as X
-import Common.Model.User as X
diff --git a/common/src/Common/Model/Category.hs b/common/src/Common/Model/Category.hs
deleted file mode 100644
index cc3f795..0000000
--- a/common/src/Common/Model/Category.hs
+++ /dev/null
@@ -1,24 +0,0 @@
-module Common.Model.Category
- ( CategoryId
- , Category(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Int (Int64)
-import Data.Text (Text)
-import Data.Time (UTCTime)
-import GHC.Generics (Generic)
-
-type CategoryId = Int64
-
-data Category = Category
- { _category_id :: CategoryId
- , _category_name :: Text
- , _category_color :: Text
- , _category_createdAt :: UTCTime
- , _category_editedAt :: Maybe UTCTime
- , _category_deletedAt :: Maybe UTCTime
- } deriving (Eq, Show, Generic)
-
-instance FromJSON Category
-instance ToJSON Category
diff --git a/common/src/Common/Model/CategoryPage.hs b/common/src/Common/Model/CategoryPage.hs
deleted file mode 100644
index e20f49f..0000000
--- a/common/src/Common/Model/CategoryPage.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Common.Model.CategoryPage
- ( CategoryPage(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import GHC.Generics (Generic)
-
-import Common.Model.Category (Category, CategoryId)
-
-data CategoryPage = CategoryPage
- { _categoryPage_page :: Int
- , _categoryPage_categories :: [Category]
- , _categoryPage_usedCategories :: [CategoryId]
- , _categoryPage_totalCount :: Int
- } deriving (Eq, Show, Generic)
-
-instance FromJSON CategoryPage
-instance ToJSON CategoryPage
diff --git a/common/src/Common/Model/CreateCategoryForm.hs b/common/src/Common/Model/CreateCategoryForm.hs
deleted file mode 100644
index 4668ef4..0000000
--- a/common/src/Common/Model/CreateCategoryForm.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module Common.Model.CreateCategoryForm
- ( CreateCategoryForm(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Text (Text)
-import GHC.Generics (Generic)
-
-data CreateCategoryForm = CreateCategoryForm
- { _createCategoryForm_name :: Text
- , _createCategoryForm_color :: Text
- } deriving (Show, Generic)
-
-instance FromJSON CreateCategoryForm
-instance ToJSON CreateCategoryForm
diff --git a/common/src/Common/Model/CreateIncomeForm.hs b/common/src/Common/Model/CreateIncomeForm.hs
deleted file mode 100644
index e83bf0a..0000000
--- a/common/src/Common/Model/CreateIncomeForm.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module Common.Model.CreateIncomeForm
- ( CreateIncomeForm(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Text (Text)
-import GHC.Generics (Generic)
-
-data CreateIncomeForm = CreateIncomeForm
- { _createIncomeForm_amount :: Text
- , _createIncomeForm_date :: Text
- } deriving (Show, Generic)
-
-instance FromJSON CreateIncomeForm
-instance ToJSON CreateIncomeForm
diff --git a/common/src/Common/Model/CreatePaymentForm.hs b/common/src/Common/Model/CreatePaymentForm.hs
deleted file mode 100644
index 60c5423..0000000
--- a/common/src/Common/Model/CreatePaymentForm.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Common.Model.CreatePaymentForm
- ( CreatePaymentForm(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Text (Text)
-import GHC.Generics (Generic)
-
-import Common.Model.Category (CategoryId)
-import Common.Model.Frequency (Frequency)
-
-data CreatePaymentForm = CreatePaymentForm
- { _createPaymentForm_name :: Text
- , _createPaymentForm_cost :: Text
- , _createPaymentForm_date :: Text
- , _createPaymentForm_category :: CategoryId
- , _createPaymentForm_frequency :: Frequency
- } deriving (Show, Generic)
-
-instance FromJSON CreatePaymentForm
-instance ToJSON CreatePaymentForm
diff --git a/common/src/Common/Model/Currency.hs b/common/src/Common/Model/Currency.hs
deleted file mode 100644
index 175aeba..0000000
--- a/common/src/Common/Model/Currency.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-module Common.Model.Currency
- ( Currency(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Text (Text)
-import GHC.Generics (Generic)
-
-newtype Currency = Currency Text deriving (Show, Generic)
-
-instance FromJSON Currency
-instance ToJSON Currency
diff --git a/common/src/Common/Model/EditCategoryForm.hs b/common/src/Common/Model/EditCategoryForm.hs
deleted file mode 100644
index a2ceca0..0000000
--- a/common/src/Common/Model/EditCategoryForm.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Common.Model.EditCategoryForm
- ( EditCategoryForm(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Text (Text)
-import GHC.Generics (Generic)
-
-import Common.Model.Category (CategoryId)
-
-data EditCategoryForm = EditCategoryForm
- { _editCategoryForm_id :: CategoryId
- , _editCategoryForm_name :: Text
- , _editCategoryForm_color :: Text
- } deriving (Show, Generic)
-
-instance FromJSON EditCategoryForm
-instance ToJSON EditCategoryForm
diff --git a/common/src/Common/Model/EditIncome.hs b/common/src/Common/Model/EditIncome.hs
deleted file mode 100644
index 0e65f12..0000000
--- a/common/src/Common/Model/EditIncome.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-module Common.Model.EditIncome
- ( EditIncome(..)
- ) where
-
-import Data.Aeson (FromJSON)
-import Data.Time.Calendar (Day)
-import GHC.Generics (Generic)
-
-import Common.Model.Income (IncomeId)
-
-data EditIncome = EditIncome
- { _editIncome_id :: IncomeId
- , _editIncome_date :: Day
- , _editIncome_amount :: Int
- } deriving (Show, Generic)
-
-instance FromJSON EditIncome
diff --git a/common/src/Common/Model/EditIncomeForm.hs b/common/src/Common/Model/EditIncomeForm.hs
deleted file mode 100644
index ff975fc..0000000
--- a/common/src/Common/Model/EditIncomeForm.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Common.Model.EditIncomeForm
- ( EditIncomeForm(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Text (Text)
-import GHC.Generics (Generic)
-
-import Common.Model.Income (IncomeId)
-
-data EditIncomeForm = EditIncomeForm
- { _editIncomeForm_id :: IncomeId
- , _editIncomeForm_amount :: Text
- , _editIncomeForm_date :: Text
- } deriving (Show, Generic)
-
-instance FromJSON EditIncomeForm
-instance ToJSON EditIncomeForm
diff --git a/common/src/Common/Model/EditPaymentForm.hs b/common/src/Common/Model/EditPaymentForm.hs
deleted file mode 100644
index 168c9ff..0000000
--- a/common/src/Common/Model/EditPaymentForm.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-module Common.Model.EditPaymentForm
- ( EditPaymentForm(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Text (Text)
-import GHC.Generics (Generic)
-
-import Common.Model.Category (CategoryId)
-import Common.Model.Frequency (Frequency)
-import Common.Model.Payment (PaymentId)
-
-data EditPaymentForm = EditPaymentForm
- { _editPaymentForm_id :: PaymentId
- , _editPaymentForm_name :: Text
- , _editPaymentForm_cost :: Text
- , _editPaymentForm_date :: Text
- , _editPaymentForm_category :: CategoryId
- , _editPaymentForm_frequency :: Frequency
- } deriving (Show, Generic)
-
-instance FromJSON EditPaymentForm
-instance ToJSON EditPaymentForm
diff --git a/common/src/Common/Model/Email.hs b/common/src/Common/Model/Email.hs
deleted file mode 100644
index e938f83..0000000
--- a/common/src/Common/Model/Email.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-module Common.Model.Email
- ( Email(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Text (Text)
-import GHC.Generics (Generic)
-
-newtype Email = Email Text deriving (Show, Generic)
-
-instance FromJSON Email
-instance ToJSON Email
diff --git a/common/src/Common/Model/ExceedingPayer.hs b/common/src/Common/Model/ExceedingPayer.hs
deleted file mode 100644
index b7d3efb..0000000
--- a/common/src/Common/Model/ExceedingPayer.hs
+++ /dev/null
@@ -1,16 +0,0 @@
-module Common.Model.ExceedingPayer
- ( ExceedingPayer(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import GHC.Generics (Generic)
-
-import Common.Model.User (UserId)
-
-data ExceedingPayer = ExceedingPayer
- { _exceedingPayer_userId :: UserId
- , _exceedingPayer_amount :: Int
- } deriving (Eq, Show, Generic)
-
-instance FromJSON ExceedingPayer
-instance ToJSON ExceedingPayer
diff --git a/common/src/Common/Model/Frequency.hs b/common/src/Common/Model/Frequency.hs
deleted file mode 100644
index 48e75ea..0000000
--- a/common/src/Common/Model/Frequency.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Common.Model.Frequency
- ( Frequency(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import GHC.Generics (Generic)
-
-data Frequency =
- Punctual
- | Monthly
- deriving (Eq, Read, Show, Generic, Ord)
-
-instance FromJSON Frequency
-instance ToJSON Frequency
diff --git a/common/src/Common/Model/Income.hs b/common/src/Common/Model/Income.hs
deleted file mode 100644
index 57d07f1..0000000
--- a/common/src/Common/Model/Income.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Common.Model.Income
- ( IncomeId
- , Income(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Int (Int64)
-import Data.Time (UTCTime)
-import Data.Time.Calendar (Day)
-import GHC.Generics (Generic)
-
-import Common.Model.User (UserId)
-
-type IncomeId = Int64
-
-data Income = Income
- { _income_id :: IncomeId
- , _income_userId :: UserId
- , _income_date :: Day
- , _income_amount :: Int
- , _income_createdAt :: UTCTime
- , _income_editedAt :: Maybe UTCTime
- , _income_deletedAt :: Maybe UTCTime
- } deriving (Eq, Show, Generic)
-
-instance FromJSON Income
-instance ToJSON Income
diff --git a/common/src/Common/Model/IncomeHeader.hs b/common/src/Common/Model/IncomeHeader.hs
deleted file mode 100644
index 7e712e8..0000000
--- a/common/src/Common/Model/IncomeHeader.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Common.Model.IncomeHeader
- ( IncomeHeader(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Map (Map)
-import Data.Time.Calendar (Day)
-import GHC.Generics (Generic)
-
-import Common.Model.User (UserId)
-
-data IncomeHeader = IncomeHeader
- { _incomeHeader_since :: Maybe Day
- , _incomeHeader_byUser :: Map UserId Int
- } deriving (Eq, Show, Generic)
-
-instance FromJSON IncomeHeader
-instance ToJSON IncomeHeader
diff --git a/common/src/Common/Model/IncomePage.hs b/common/src/Common/Model/IncomePage.hs
deleted file mode 100644
index 977b0ea..0000000
--- a/common/src/Common/Model/IncomePage.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Common.Model.IncomePage
- ( IncomePage(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import GHC.Generics (Generic)
-
-import Common.Model.Income (Income)
-import Common.Model.IncomeHeader (IncomeHeader)
-
-data IncomePage = IncomePage
- { _incomePage_page :: Int
- , _incomePage_header :: IncomeHeader
- , _incomePage_incomes :: [Income]
- , _incomePage_totalCount :: Int
- } deriving (Eq, Show, Generic)
-
-instance FromJSON IncomePage
-instance ToJSON IncomePage
diff --git a/common/src/Common/Model/Init.hs b/common/src/Common/Model/Init.hs
deleted file mode 100644
index 5ef1535..0000000
--- a/common/src/Common/Model/Init.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Common.Model.Init
- ( Init(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import GHC.Generics (Generic)
-
-import Common.Model.Currency (Currency)
-import Common.Model.User (User, UserId)
-
-data Init = Init
- { _init_users :: [User]
- , _init_currentUser :: UserId
- , _init_currency :: Currency
- } deriving (Show, Generic)
-
-instance FromJSON Init
-instance ToJSON Init
diff --git a/common/src/Common/Model/Password.hs b/common/src/Common/Model/Password.hs
deleted file mode 100644
index 1b51a47..0000000
--- a/common/src/Common/Model/Password.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-module Common.Model.Password
- ( Password(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Text (Text)
-import GHC.Generics (Generic)
-
-newtype Password = Password Text deriving (Show, Generic)
-
-instance FromJSON Password
-instance ToJSON Password
diff --git a/common/src/Common/Model/Payment.hs b/common/src/Common/Model/Payment.hs
deleted file mode 100644
index 733a145..0000000
--- a/common/src/Common/Model/Payment.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Common.Model.Payment
- ( PaymentId
- , Payment(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Int (Int64)
-import Data.Text (Text)
-import Data.Time (UTCTime)
-import Data.Time.Calendar (Day)
-import GHC.Generics (Generic)
-
-import Common.Model.Category (CategoryId)
-import Common.Model.Frequency
-import Common.Model.User (UserId)
-
-type PaymentId = Int64
-
-data Payment = Payment
- { _payment_id :: PaymentId
- , _payment_user :: UserId
- , _payment_name :: Text
- , _payment_cost :: Int
- , _payment_date :: Day
- , _payment_category :: CategoryId
- , _payment_frequency :: Frequency
- , _payment_createdAt :: UTCTime
- , _payment_editedAt :: Maybe UTCTime
- , _payment_deletedAt :: Maybe UTCTime
- } deriving (Eq, Show, Generic)
-
-instance FromJSON Payment
-instance ToJSON Payment
diff --git a/common/src/Common/Model/PaymentHeader.hs b/common/src/Common/Model/PaymentHeader.hs
deleted file mode 100644
index 35f5e1a..0000000
--- a/common/src/Common/Model/PaymentHeader.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Common.Model.PaymentHeader
- ( PaymentHeader(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Map (Map)
-import GHC.Generics (Generic)
-
-import Common.Model.ExceedingPayer (ExceedingPayer)
-import Common.Model.User (UserId)
-
-data PaymentHeader = PaymentHeader
- { _paymentHeader_exceedingPayers :: [ExceedingPayer]
- , _paymentHeader_repartition :: Map UserId Int
- } deriving (Eq, Show, Generic)
-
-instance FromJSON PaymentHeader
-instance ToJSON PaymentHeader
diff --git a/common/src/Common/Model/PaymentPage.hs b/common/src/Common/Model/PaymentPage.hs
deleted file mode 100644
index 88d9715..0000000
--- a/common/src/Common/Model/PaymentPage.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Common.Model.PaymentPage
- ( PaymentPage(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import GHC.Generics (Generic)
-
-import Common.Model.Frequency (Frequency)
-import Common.Model.Payment (Payment)
-import Common.Model.PaymentHeader (PaymentHeader)
-
-data PaymentPage = PaymentPage
- { _paymentPage_page :: Int
- , _paymentPage_frequency :: Frequency
- , _paymentPage_header :: PaymentHeader
- , _paymentPage_payments :: [Payment]
- , _paymentPage_totalCount :: Int
- } deriving (Eq, Show, Generic)
-
-instance FromJSON PaymentPage
-instance ToJSON PaymentPage
diff --git a/common/src/Common/Model/SignInForm.hs b/common/src/Common/Model/SignInForm.hs
deleted file mode 100644
index 7a25935..0000000
--- a/common/src/Common/Model/SignInForm.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module Common.Model.SignInForm
- ( SignInForm(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Text (Text)
-import GHC.Generics (Generic)
-
-data SignInForm = SignInForm
- { _signInForm_email :: Text
- , _signInForm_password :: Text
- } deriving (Show, Generic)
-
-instance FromJSON SignInForm
-instance ToJSON SignInForm
diff --git a/common/src/Common/Model/Stats.hs b/common/src/Common/Model/Stats.hs
deleted file mode 100644
index 86e6ab9..0000000
--- a/common/src/Common/Model/Stats.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-module Common.Model.Stats
- ( Stats
- , MonthStats(..)
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Map (Map)
-import Data.Time.Calendar (Day)
-import GHC.Generics (Generic)
-
-import Common.Model.Category (CategoryId)
-import Common.Model.User (UserId)
-
-type Stats = [MonthStats]
-
-data MonthStats = MonthStats
- { _monthStats_start :: Day
- , _monthStats_paymentsByCategory :: Map CategoryId Int
- , _monthStats_incomeByUser :: Map UserId Int
- } deriving (Eq, Show, Generic)
-
-instance FromJSON MonthStats
-instance ToJSON MonthStats
diff --git a/common/src/Common/Model/User.hs b/common/src/Common/Model/User.hs
deleted file mode 100644
index a30d104..0000000
--- a/common/src/Common/Model/User.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Common.Model.User
- ( UserId
- , User(..)
- , findUser
- ) where
-
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Int (Int64)
-import qualified Data.List as L
-import Data.Text (Text)
-import Data.Time (UTCTime)
-import GHC.Generics (Generic)
-
-type UserId = Int64
-
-data User = User
- { _user_id :: UserId
- , _user_creation :: UTCTime
- , _user_email :: Text
- , _user_name :: Text
- } deriving (Show, Generic)
-
-instance FromJSON User
-instance ToJSON User
-
-findUser :: UserId -> [User] -> Maybe User
-findUser userId users = L.find ((== userId) . _user_id) users
diff --git a/common/src/Common/Msg.hs b/common/src/Common/Msg.hs
deleted file mode 100644
index 9e4cfe2..0000000
--- a/common/src/Common/Msg.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module Common.Msg
- ( get
- , Key(..)
- ) where
-
-import Data.Text (Text)
-
-import Common.Message.Key (Key (..))
-import Common.Message.Lang (Lang (..))
-import qualified Common.Message.Translation as Translation
-
-get :: Key -> Text
-get = Translation.get French
diff --git a/common/src/Common/Util/Text.hs b/common/src/Common/Util/Text.hs
deleted file mode 100644
index 0f9c187..0000000
--- a/common/src/Common/Util/Text.hs
+++ /dev/null
@@ -1,49 +0,0 @@
-module Common.Util.Text
- ( search
- , formatSearch
- , unaccent
- ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-
-search :: Text -> Text -> Bool
-search s t = (formatSearch s) `T.isInfixOf` (formatSearch t)
-
-formatSearch :: Text -> Text
-formatSearch = T.toLower . unaccent
-
-unaccent :: Text -> Text
-unaccent = T.map unaccentChar
-
-unaccentChar :: Char -> Char
-unaccentChar c = case c of
- 'à' -> 'a'
- 'á' -> 'a'
- 'â' -> 'a'
- 'ã' -> 'a'
- 'ä' -> 'a'
- 'ç' -> 'c'
- 'è' -> 'e'
- 'é' -> 'e'
- 'ê' -> 'e'
- 'ë' -> 'e'
- 'ì' -> 'i'
- 'í' -> 'i'
- 'î' -> 'i'
- 'ï' -> 'i'
- 'ñ' -> 'n'
- 'ò' -> 'o'
- 'ó' -> 'o'
- 'ô' -> 'o'
- 'õ' -> 'o'
- 'ö' -> 'o'
- 'š' -> 's'
- 'ù' -> 'u'
- 'ú' -> 'u'
- 'û' -> 'u'
- 'ü' -> 'u'
- 'ý' -> 'y'
- 'ÿ' -> 'y'
- 'ž' -> 'z'
- _ -> c
diff --git a/common/src/Common/Util/Time.hs b/common/src/Common/Util/Time.hs
deleted file mode 100644
index 6240720..0000000
--- a/common/src/Common/Util/Time.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Common.Util.Time
- ( timeToDay
- , parseDay
- ) where
-
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time (UTCTime)
-import qualified Data.Time as Time
-import Data.Time.Calendar (Day)
-import Data.Time.LocalTime
-import qualified Text.Read as T
-
-timeToDay :: UTCTime -> IO Day
-timeToDay time = localDay . (flip utcToLocalTime time) <$> getTimeZone time
-
-parseDay :: Text -> Maybe Day
-parseDay str = do
- (y, m, d) <-
- case T.splitOn "-" str of
- [y, m, d] -> Just (y, m, d)
- _ -> Nothing
- d' <- T.readMaybe . T.unpack $ d
- m' <- T.readMaybe . T.unpack $ m
- y' <- T.readMaybe . T.unpack $ y
- return $ Time.fromGregorian y' m' d'
diff --git a/common/src/Common/Util/Validation.hs b/common/src/Common/Util/Validation.hs
deleted file mode 100644
index f195d95..0000000
--- a/common/src/Common/Util/Validation.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module Common.Util.Validation
- ( isSuccess
- , isFailure
- ) where
-
-import Data.Validation (Validation (Failure, Success))
-
-isSuccess :: forall a b. Validation a b -> Bool
-isSuccess (Failure _) = False
-isSuccess (Success _) = True
-
-isFailure :: forall a b. Validation a b -> Bool
-isFailure = not . isSuccess
diff --git a/common/src/Common/Validation/Atomic.hs b/common/src/Common/Validation/Atomic.hs
deleted file mode 100644
index 9c21e14..0000000
--- a/common/src/Common/Validation/Atomic.hs
+++ /dev/null
@@ -1,61 +0,0 @@
-module Common.Validation.Atomic
- ( color
- , day
- , minLength
- , nonEmpty
- , nonNullNumber
- , number
- , password
- ) where
-
-import qualified Data.Char as Char
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time.Calendar (Day)
-import Data.Validation (Validation)
-import qualified Data.Validation as V
-import qualified Text.Read as T
-
-import qualified Common.Msg as Msg
-import qualified Common.Util.Time as Time
-
-minLength :: Int -> Text -> Validation Text Text
-minLength l =
- V.validate
- (Msg.get (Msg.Form_MinChars l))
- (\t -> if T.length t >= l then Just t else Nothing)
-
-nonEmpty :: Text -> Validation Text Text
-nonEmpty =
- V.validate
- (Msg.get Msg.Form_NonEmpty)
- (\t -> if (not . T.null $ t) then Just t else Nothing)
-
-number :: Text -> Validation Text Int
-number input =
- case (T.readMaybe . T.unpack $ input) of
- Just n -> V.Success n
- _ -> V.Failure (Msg.get Msg.Form_InvalidInt)
-
-nonNullNumber :: Int -> Validation Text Int
-nonNullNumber =
- V.validate
- (Msg.get Msg.Form_NonNullNumber)
- (\n -> if n /= 0 then Just n else Nothing)
-
-day :: Text -> Validation Text Day
-day str =
- case Time.parseDay str of
- Just d -> V.Success d
- Nothing -> V.Failure $ Msg.get Msg.Form_InvalidDate
-
-color :: Text -> Validation Text Text
-color str =
- if T.take 1 str == "#" && T.all Char.isHexDigit (T.drop 1 str) then
- V.Success str
-
- else
- V.Failure (Msg.get Msg.Form_InvalidColor)
-
-password :: Text -> Validation Text Text
-password = minLength 8
diff --git a/common/src/Common/Validation/Category.hs b/common/src/Common/Validation/Category.hs
deleted file mode 100644
index f9e6ab4..0000000
--- a/common/src/Common/Validation/Category.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module Common.Validation.Category
- ( name
- , color
- ) where
-
-import Data.Text (Text)
-import Data.Validation (Validation)
-
-import qualified Common.Validation.Atomic as Atomic
-
-name :: Text -> Validation Text Text
-name = Atomic.nonEmpty
-
-color :: Text -> Validation Text Text
-color = Atomic.color
diff --git a/common/src/Common/Validation/Income.hs b/common/src/Common/Validation/Income.hs
deleted file mode 100644
index 7a58bab..0000000
--- a/common/src/Common/Validation/Income.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-module Common.Validation.Income
- ( amount
- , date
- ) where
-
-import Data.Text (Text)
-import Data.Time.Calendar (Day)
-import Data.Validation (Validation)
-import qualified Data.Validation as V
-
-import qualified Common.Validation.Atomic as Atomic
-
-amount :: Text -> Validation Text Int
-amount input = V.bindValidation (Atomic.number input) Atomic.nonNullNumber
-
-date :: Text -> Validation Text Day
-date = Atomic.day
diff --git a/common/src/Common/Validation/Payment.hs b/common/src/Common/Validation/Payment.hs
deleted file mode 100644
index e3c447a..0000000
--- a/common/src/Common/Validation/Payment.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Common.Validation.Payment
- ( name
- , cost
- , date
- , category
- ) where
-
-import Data.Text (Text)
-import Data.Time.Calendar (Day)
-import Data.Validation (Validation)
-import qualified Data.Validation as V
-
-import Common.Model (CategoryId)
-import qualified Common.Msg as Msg
-import qualified Common.Validation.Atomic as Atomic
-
-name :: Text -> Validation Text Text
-name = Atomic.nonEmpty
-
-cost :: Text -> Validation Text Int
-cost input = V.bindValidation (Atomic.number input) Atomic.nonNullNumber
-
-date :: Text -> Validation Text Day
-date = Atomic.day
-
-category :: [CategoryId] -> CategoryId -> Validation Text CategoryId
-category cs c =
- if elem c cs then
- V.Success c
- else
- V.Failure $ Msg.get Msg.Form_InvalidCategory
diff --git a/common/src/Common/Validation/SignIn.hs b/common/src/Common/Validation/SignIn.hs
deleted file mode 100644
index ac9cc37..0000000
--- a/common/src/Common/Validation/SignIn.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-module Common.Validation.SignIn
- ( email
- , password
- ) where
-
-import Data.Text (Text)
-import Data.Validation (Validation)
-
-import Common.Model.Email (Email (..))
-import Common.Model.Password (Password (..))
-import qualified Common.Validation.Atomic as Atomic
-
-email :: Text -> Validation Text Email
-email = fmap Email . Atomic.minLength 5
-
-password :: Text -> Validation Text Password
-password = fmap Password . Atomic.minLength 8
diff --git a/common/src/Common/View/Format.hs b/common/src/Common/View/Format.hs
deleted file mode 100644
index 5d879fa..0000000
--- a/common/src/Common/View/Format.hs
+++ /dev/null
@@ -1,78 +0,0 @@
-module Common.View.Format
- ( shortDay
- , longDay
- , price
- , number
- , monthAndYear
- ) where
-
-import qualified Data.List as L
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time.Calendar (Day)
-import qualified Data.Time.Calendar as Calendar
-
-import Common.Model (Currency (..))
-import Common.Msg (Key)
-import qualified Common.Msg as Msg
-
-shortDay :: Day -> Text
-shortDay date =
- Msg.get $ Msg.Date_Short
- day
- month
- (fromIntegral year)
- where (year, month, day) = Calendar.toGregorian date
-
-longDay :: Day -> Text
-longDay date =
- Msg.get $ Msg.Date_Long
- day
- (Maybe.fromMaybe "−" . fmap Msg.get . monthToKey $ month)
- (fromIntegral year)
- where (year, month, day) = Calendar.toGregorian date
-
-monthAndYear :: Day -> Text
-monthAndYear date =
- T.intercalate " "
- [ Maybe.fromMaybe "" . fmap ((\t -> T.concat [t, " "]) . Msg.get) . monthToKey $ month
- , T.pack . show $ year
- ]
- where (year, month, _) = Calendar.toGregorian date
-
-monthToKey :: Int -> Maybe Key
-monthToKey 1 = Just Msg.Month_January
-monthToKey 2 = Just Msg.Month_February
-monthToKey 3 = Just Msg.Month_March
-monthToKey 4 = Just Msg.Month_April
-monthToKey 5 = Just Msg.Month_May
-monthToKey 6 = Just Msg.Month_June
-monthToKey 7 = Just Msg.Month_July
-monthToKey 8 = Just Msg.Month_August
-monthToKey 9 = Just Msg.Month_September
-monthToKey 10 = Just Msg.Month_October
-monthToKey 11 = Just Msg.Month_November
-monthToKey 12 = Just Msg.Month_December
-monthToKey _ = Nothing
-
-price :: Currency -> Int -> Text
-price (Currency currency) amount = T.concat [ number amount, " ", currency ]
-
-number :: Int -> Text
-number n =
- T.pack
- . (++) (if n < 0 then "-" else "")
- . reverse
- . concat
- . L.intersperse " "
- . group 3
- . reverse
- . show
- . abs $ n
-
-group :: Int -> [a] -> [[a]]
-group n xs =
- if length xs <= n
- then [xs]
- else (take n xs) : (group n (drop n xs))
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..488b2f9
--- /dev/null
+++ b/config.json
@@ -0,0 +1,4 @@
+{
+ "secure_cookies": false,
+ "mock_mails": true
+}
diff --git a/default.nix b/default.nix
deleted file mode 100644
index fbb3725..0000000
--- a/default.nix
+++ /dev/null
@@ -1,25 +0,0 @@
-with import ./nix/nixpkgs.nix {};
-
-let
- reflex-platform = import (pkgs.fetchFromGitHub {
- owner = "reflex-frp";
- repo = "reflex-platform";
-
- # Mon Jul 29 15:48:55 2019 -0400
- rev = "51e02339704b7502e63bccf10a72fa4dda744b17";
- sha256 = "1mkimidf755968xzbm3z222xgpdvgg6xmmrfppv1hw0rap5w53iw";
- }) {};
-in
- reflex-platform.project ({ pkgs, ... }: {
- packages = {
- validation = ./validation;
- common = ./common;
- server = ./server;
- client = ./client;
- };
-
- shells = {
- ghc = [ "validation" "common" "server" ];
- ghcjs = [ "validation" "common" "client" ];
- };
- })
diff --git a/docs/balance.png b/docs/balance.png
new file mode 100644
index 0000000..85bfc82
--- /dev/null
+++ b/docs/balance.png
Binary files differ
diff --git a/docs/payments.png b/docs/payments.png
new file mode 100644
index 0000000..d811cde
--- /dev/null
+++ b/docs/payments.png
Binary files differ
diff --git a/docs/statistics.png b/docs/statistics.png
new file mode 100644
index 0000000..6284536
--- /dev/null
+++ b/docs/statistics.png
Binary files differ
diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix
deleted file mode 100644
index 9d61277..0000000
--- a/nix/nixpkgs.nix
+++ /dev/null
@@ -1,6 +0,0 @@
-import ((import <nixpkgs> {}).fetchFromGitHub {
- owner = "NixOS";
- repo = "nixpkgs";
- rev = "19.09";
- sha256 = "1ib96has10v5nr6bzf7v8kw7yzww8zanxgw2qi1ll1sbv6kj6zpd";
-})
diff --git a/nix/tools.nix b/nix/tools.nix
deleted file mode 100644
index c14e4e2..0000000
--- a/nix/tools.nix
+++ /dev/null
@@ -1,15 +0,0 @@
-with import ./nixpkgs.nix {};
-
-{
- env = stdenv.mkDerivation {
- name = "tools";
- buildInputs = with pkgs; with nodePackages; [
- nodemon
- sqlite
- cabal-install
- tmux
- tmuxinator
- haskellPackages.stylish-haskell
- ];
- };
-}
diff --git a/public/css/reset.css b/public/css/reset.css
deleted file mode 100644
index 2eecc94..0000000
--- a/public/css/reset.css
+++ /dev/null
@@ -1,60 +0,0 @@
-html, body, div, span, applet, object, iframe,
-h1, h2, h3, h4, h5, h6, p, blockquote, pre,
-a, abbr, acronym, address, big, cite, code,
-del, dfn, em, img, ins, kbd, q, s, samp,
-small, strike, strong, sub, sup, tt, var,
-b, u, i, center,
-dl, dt, dd, ol, ul, li,
-fieldset, form, label, legend,
-table, caption, tbody, tfoot, thead, tr, th, td,
-article, aside, canvas, details, embed,
-figure, figcaption, footer, header, hgroup,
-menu, nav, output, ruby, section, summary,
-time, mark, audio, video {
- margin: 0;
- padding: 0;
- border: 0;
- font-size: 100%;
- font: inherit;
- vertical-align: baseline;
-}
-/* HTML5 display-role reset for older browsers */
-article, aside, details, figcaption, figure,
-footer, header, hgroup, menu, nav, section {
- display: block;
-}
-body {
- line-height: 1;
-}
-ol, ul {
- list-style: none;
-}
-blockquote, q {
- quotes: none;
-}
-blockquote:before, blockquote:after,
-q:before, q:after {
- content: '';
- content: none;
-}
-table {
- border-collapse: collapse;
- border-spacing: 0;
-}
-
-a {
- text-decoration: none;
- color: inherit;
-}
-
-button {
- padding: 0;
- border: none;
- background-color: transparent;
-}
-button:hover { cursor: pointer; }
-button::-moz-focus-inner { border: 0; }
-:focus { outline: none; }
-
-html { box-sizing: border-box; }
-*, *:before, *:after { box-sizing: inherit; }
diff --git a/public/javascript/.gitkeep b/public/javascript/.gitkeep
deleted file mode 100644
index e69de29..0000000
--- a/public/javascript/.gitkeep
+++ /dev/null
diff --git a/server/LICENSE b/server/LICENSE
deleted file mode 100644
index 45644ff..0000000
--- a/server/LICENSE
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- <one line to give the program's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- <program> Copyright (C) <year> <name of author>
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<http://www.gnu.org/licenses/>.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/server/Setup.hs b/server/Setup.hs
deleted file mode 100644
index 4467109..0000000
--- a/server/Setup.hs
+++ /dev/null
@@ -1,2 +0,0 @@
-import Distribution.Simple
-main = defaultMain
diff --git a/server/server.cabal b/server/server.cabal
deleted file mode 100644
index 5427385..0000000
--- a/server/server.cabal
+++ /dev/null
@@ -1,131 +0,0 @@
-Name: server
-Version: 0.0.1
-License: GPL-3
-License-file: LICENSE
-Author: Joris Guyonvarch
-Maintainer: joris@guyonvarch.me
-Category: Web
-Build-type: Simple
-Cabal-version: >=1.10
-
-Executable server
- Main-is: Main.hs
- Ghc-options: -Wall -Werror
- Hs-source-dirs: src
- Default-language: Haskell2010
-
- Default-extensions:
- ExistentialQuantification
- LambdaCase
- MultiParamTypeClasses
- OverloadedStrings
- ScopedTypeVariables
-
- Build-depends:
- aeson
- , base >= 4.11 && < 5
- , base64-bytestring
- , bcrypt
- , blaze-builder
- , blaze-html
- , bytestring
- , clay
- , clientsession
- , common
- , config-manager
- , containers
- , cookie
- , filepath
- , http-conduit
- , http-types
- , jsaddle
- , mime-mail
- , monad-logger
- , mtl
- , parsec
- , process
- , random
- , resourcet
- , scotty
- , sqlite-simple
- , text
- , time
- , transformers
- , unordered-containers
- , uuid
- , validation
- , wai
- , wai-extra
- , wai-middleware-static
-
- other-modules:
- Conf
- Controller.Category
- Controller.Helper
- Controller.Income
- Controller.Index
- Controller.Payment
- Controller.Statistics
- Controller.User
- Cookie
- Design.Appearing
- Design.Color
- Design.Constants
- Design.Errors
- Design.Form
- Design.Global
- Design.Helper
- Design.Loadable
- Design.Media
- Design.Modal
- Design.Tooltip
- Design.View.ConfirmDialog
- Design.View.Header
- Design.View.NotFound
- Design.View.Pages
- Design.View.Payment
- Design.View.Payment.Form
- Design.View.Payment.HeaderForm
- Design.View.Payment.HeaderInfos
- Design.View.SignIn
- Design.View.Stat
- Design.View.Table
- Design.Views
- Job.Daemon
- Job.Frequency
- Job.Kind
- Job.Model
- Job.MonthlyPayment
- Job.WeeklyReport
- LoginSession
- Model.CreateCategory
- Model.CreateIncome
- Model.CreatePayment
- Model.EditCategory
- Model.EditIncome
- Model.EditPayment
- Model.HashedPassword
- Model.IncomeResource
- Model.Mail
- Model.PaymentResource
- Model.Query
- Model.SignIn
- Model.UUID
- Payer
- Persistence.Category
- Persistence.Frequency
- Persistence.Income
- Persistence.Payment
- Persistence.User
- Persistence.Util
- Resource
- Secure
- SendMail
- Statistics
- Util.Time
- Validation.Category
- Validation.Income
- Validation.Payment
- Validation.SignIn
- View.Mail.WeeklyReport
- View.Page
diff --git a/server/src/Conf.hs b/server/src/Conf.hs
deleted file mode 100644
index ca19c8d..0000000
--- a/server/src/Conf.hs
+++ /dev/null
@@ -1,39 +0,0 @@
-module Conf
- ( get
- , Conf(..)
- ) where
-
-import qualified Data.ConfigManager as Conf
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time.Clock (NominalDiffTime)
-
-import Common.Model (Currency (..))
-
-data Conf = Conf
- { hostname :: Text
- , port :: Int
- , signInExpiration :: NominalDiffTime
- , currency :: Currency
- , noReplyMail :: Text
- , https :: Bool
- , devMode :: Bool
- } deriving Show
-
-get :: FilePath -> IO Conf
-get path = do
- conf <-
- (flip fmap) (Conf.readConfig path) (\configOrError -> do
- conf <- configOrError
- Conf <$>
- Conf.lookup "hostname" conf <*>
- Conf.lookup "port" conf <*>
- Conf.lookup "signInExpiration" conf <*>
- fmap Currency (Conf.lookup "currency" conf) <*>
- Conf.lookup "noReplyMail" conf <*>
- Conf.lookup "https" conf <*>
- Conf.lookup "devMode" conf
- )
- case conf of
- Left msg -> error (T.unpack msg)
- Right c -> return c
diff --git a/server/src/Controller/Category.hs b/server/src/Controller/Category.hs
deleted file mode 100644
index 371ba78..0000000
--- a/server/src/Controller/Category.hs
+++ /dev/null
@@ -1,88 +0,0 @@
-module Controller.Category
- ( listAll
- , list
- , create
- , edit
- , delete
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import qualified Data.Text.Lazy as TL
-import Data.Validation (Validation (..))
-import Network.HTTP.Types.Status (badRequest400, ok200)
-import Web.Scotty hiding (delete)
-
-import Common.Model (CategoryId, CategoryPage (..),
- CreateCategoryForm (..),
- EditCategoryForm (..))
-import qualified Common.Msg as Msg
-
-import qualified Controller.Helper as ControllerHelper
-import Model.CreateCategory (CreateCategory (..))
-import Model.EditCategory (EditCategory (..))
-import qualified Model.Query as Query
-import qualified Persistence.Category as CategoryPersistence
-import qualified Persistence.Payment as PaymentPersistence
-import qualified Secure
-import qualified Validation.Category as CategoryValidation
-
-listAll :: ActionM ()
-listAll =
- Secure.loggedAction (\_ ->
- (liftIO . Query.run $ CategoryPersistence.listAll) >>= json
- )
-
-list :: Int -> Int -> ActionM ()
-list page perPage =
- Secure.loggedAction (\_ ->
- (liftIO . Query.run $ do
- categories <- CategoryPersistence.list page perPage
- usedCategories <- PaymentPersistence.usedCategories
- count <- CategoryPersistence.count
- return $ CategoryPage page categories usedCategories count
- ) >>= json
- )
-
-create :: CreateCategoryForm -> ActionM ()
-create form =
- Secure.loggedAction (\_ ->
- (liftIO . Query.run $ do
- case CategoryValidation.createCategory form of
- Success (CreateCategory name color) -> do
- Right <$> (CategoryPersistence.create name color)
-
- Failure validationError ->
- return $ Left validationError
- ) >>= ControllerHelper.okOrBadRequest
- )
-
-edit :: EditCategoryForm -> ActionM ()
-edit form =
- Secure.loggedAction (\_ ->
- (liftIO . Query.run $ do
- case CategoryValidation.editCategory form of
- Success (EditCategory categoryId name color) ->
- do
- isSuccess <- CategoryPersistence.edit categoryId name color
- return $ if isSuccess then
- Right ()
- else
- Left $ Msg.get Msg.Error_CategoryEdit
-
- Failure validationError ->
- return $ Left validationError
- ) >>= ControllerHelper.okOrBadRequest
- )
-
-delete :: CategoryId -> ActionM ()
-delete categoryId =
- Secure.loggedAction (\_ -> do
- deleted <- liftIO . Query.run $ do
- CategoryPersistence.delete categoryId
- if deleted
- then
- status ok200
- else do
- status badRequest400
- text . TL.fromStrict $ Msg.get Msg.Category_NotDeleted
- )
diff --git a/server/src/Controller/Helper.hs b/server/src/Controller/Helper.hs
deleted file mode 100644
index dc9cbc4..0000000
--- a/server/src/Controller/Helper.hs
+++ /dev/null
@@ -1,16 +0,0 @@
-module Controller.Helper
- ( okOrBadRequest
- ) where
-
-import Data.Text (Text)
-import qualified Data.Text.Lazy as LT
-import qualified Network.HTTP.Types.Status as Status
-import Web.Scotty (ActionM)
-import qualified Web.Scotty as S
-
-okOrBadRequest :: Either Text () -> ActionM ()
-okOrBadRequest (Left message) = do
- S.status Status.badRequest400
- S.text (LT.fromStrict message)
-okOrBadRequest (Right ()) =
- S.status Status.ok200
diff --git a/server/src/Controller/Income.hs b/server/src/Controller/Income.hs
deleted file mode 100644
index 96ccbbc..0000000
--- a/server/src/Controller/Income.hs
+++ /dev/null
@@ -1,90 +0,0 @@
-module Controller.Income
- ( list
- , create
- , edit
- , delete
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import qualified Data.Map as M
-import qualified Data.Time.Clock as Clock
-import Data.Validation (Validation (..))
-import qualified Network.HTTP.Types.Status as Status
-import Web.Scotty hiding (delete)
-
-import Common.Model (CreateIncomeForm (..),
- EditIncomeForm (..),
- IncomeHeader (..), IncomeId,
- IncomePage (..), User (..))
-import qualified Common.Msg as Msg
-
-import qualified Controller.Helper as ControllerHelper
-import Model.CreateIncome (CreateIncome (..))
-import Model.EditIncome (EditIncome (..))
-import qualified Model.Query as Query
-import qualified Persistence.Income as IncomePersistence
-import qualified Persistence.Payment as PaymentPersistence
-import qualified Persistence.User as UserPersistence
-import qualified Secure
-import qualified Validation.Income as IncomeValidation
-
-list :: Int -> Int -> ActionM ()
-list page perPage =
- Secure.loggedAction (\_ -> do
- currentTime <- liftIO Clock.getCurrentTime
- (liftIO . Query.run $ do
- count <- IncomePersistence.count
-
- users <- UserPersistence.list
- let userIds = _user_id <$> users
-
- paymentRange <- PaymentPersistence.getRange
- incomeDefinedForAll <- IncomePersistence.definedForAll userIds
- let since = max <$> (fst <$> paymentRange) <*> incomeDefinedForAll
-
- cumulativeIncome <-
- case since of
- Just s -> IncomePersistence.getCumulativeIncome s (Clock.utctDay currentTime)
- Nothing -> return M.empty
-
- incomes <- IncomePersistence.list page perPage
- return $ IncomePage page (IncomeHeader since cumulativeIncome) incomes count) >>= json
- )
-
-create :: CreateIncomeForm -> ActionM ()
-create form =
- Secure.loggedAction (\user ->
- (liftIO . Query.run $ do
- case IncomeValidation.createIncome form of
- Success (CreateIncome amount date) -> do
- Right <$> (IncomePersistence.create (_user_id user) date amount)
-
- Failure validationError ->
- return $ Left validationError
- ) >>= ControllerHelper.okOrBadRequest
- )
-
-edit :: EditIncomeForm -> ActionM ()
-edit form =
- Secure.loggedAction (\user ->
- (liftIO . Query.run $ do
- case IncomeValidation.editIncome form of
- Success (EditIncome incomeId amount date) ->
- do
- isSuccess <- IncomePersistence.edit (_user_id user) incomeId date amount
- return $ if isSuccess then
- Right ()
- else
- Left $ Msg.get Msg.Error_IncomeEdit
-
- Failure validationError ->
- return $ Left validationError
- ) >>= ControllerHelper.okOrBadRequest
- )
-
-delete :: IncomeId -> ActionM ()
-delete incomeId =
- Secure.loggedAction (\user -> do
- _ <- liftIO . Query.run $ IncomePersistence.delete (_user_id user) incomeId
- status Status.ok200
- )
diff --git a/server/src/Controller/Index.hs b/server/src/Controller/Index.hs
deleted file mode 100644
index 4f4ae77..0000000
--- a/server/src/Controller/Index.hs
+++ /dev/null
@@ -1,76 +0,0 @@
-module Controller.Index
- ( get
- , signIn
- , signOut
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import Data.Text (Text)
-import qualified Data.Text.Lazy as TL
-import Data.Validation (Validation (..))
-import qualified Network.HTTP.Types.Status as Status
-import Prelude hiding (error, init)
-import Web.Scotty (ActionM)
-import qualified Web.Scotty as S
-
-import Common.Model (Init (..), SignInForm (..),
- User (..))
-import qualified Common.Msg as Msg
-
-import Conf (Conf (..))
-import qualified LoginSession
-import Model.Query (Query)
-import qualified Model.Query as Query
-import Model.SignIn (SignIn (..))
-import qualified Persistence.User as UserPersistence
-import qualified Validation.SignIn as SignInValidation
-import View.Page (page)
-
-get :: Conf -> ActionM ()
-get conf = do
- init <- do
- mbToken <- LoginSession.get
- case mbToken of
- Nothing ->
- return Nothing
- Just token -> do
- liftIO . Query.run $ getInit conf token
- S.html $ page init
-
-signIn :: Conf -> SignInForm -> ActionM ()
-signIn conf form =
- case SignInValidation.signIn form of
- Failure _ ->
- textKey Status.badRequest400 Msg.SignIn_InvalidCredentials
- Success (SignIn email password) -> do
- result <- liftIO . Query.run $ do
- isPasswordValid <- UserPersistence.checkPassword email password
- if isPasswordValid then
- do
- signInToken <- UserPersistence.createSignInToken email
- init <- getInit conf signInToken
- return $ Just (signInToken, init)
- else
- return Nothing
- case result of
- Just (signInToken, init) -> do
- LoginSession.put conf signInToken
- S.json init
-
- Nothing ->
- textKey Status.badRequest400 Msg.SignIn_InvalidCredentials
- where textKey st key = S.status st >> (S.text . TL.fromStrict $ Msg.get key)
-
-getInit :: Conf -> Text -> Query (Maybe Init)
-getInit conf signInToken = do
- user <- UserPersistence.get signInToken
- case user of
- Just u ->
- do
- users <- UserPersistence.list
- return . Just $ Init users (_user_id u) (Conf.currency conf)
- Nothing ->
- return Nothing
-
-signOut :: Conf -> ActionM ()
-signOut conf = LoginSession.delete conf >> S.status Status.ok200
diff --git a/server/src/Controller/Payment.hs b/server/src/Controller/Payment.hs
deleted file mode 100644
index 4fb4d54..0000000
--- a/server/src/Controller/Payment.hs
+++ /dev/null
@@ -1,118 +0,0 @@
-module Controller.Payment
- ( list
- , create
- , edit
- , delete
- , searchCategory
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import qualified Data.Map as M
-import Data.Text (Text)
-import qualified Data.Time.Clock as Clock
-import qualified Data.Time.Calendar as Calendar
-import Data.Validation (Validation (Failure, Success))
-import Web.Scotty (ActionM)
-import qualified Web.Scotty as S
-
-import Common.Model (Category (..), CreatePaymentForm (..),
- EditPaymentForm (..), Frequency,
- PaymentHeader (..), PaymentId,
- PaymentPage (..), User (..))
-import qualified Common.Msg as Msg
-
-import qualified Controller.Helper as ControllerHelper
-import Model.CreatePayment (CreatePayment (..))
-import Model.EditPayment (EditPayment (..))
-import qualified Model.Query as Query
-import qualified Payer as Payer
-import qualified Persistence.Category as CategoryPersistence
-import qualified Persistence.Income as IncomePersistence
-import qualified Persistence.Payment as PaymentPersistence
-import qualified Persistence.User as UserPersistence
-import qualified Secure
-import qualified Validation.Payment as PaymentValidation
-
-list :: Frequency -> Int -> Int -> Text -> ActionM ()
-list frequency page perPage search =
- Secure.loggedAction (\_ -> do
- currentUtctDay <- liftIO $ Clock.utctDay <$> Clock.getCurrentTime
- (liftIO . Query.run $ do
- count <- PaymentPersistence.count frequency search
- payments <- PaymentPersistence.listActivePage frequency page perPage search
-
- users <- UserPersistence.list
-
- paymentRange <- PaymentPersistence.getRange
- incomeDefinedForAll <- IncomePersistence.definedForAll (_user_id <$> users)
-
- cumulativeIncome <-
- case (incomeDefinedForAll, paymentRange) of
- (Just incomeStart, Just (paymentStart, _)) ->
- IncomePersistence.getCumulativeIncome (max incomeStart paymentStart) currentUtctDay
-
- _ ->
- return M.empty
-
- searchRepartition <-
- case paymentRange of
- Just (from, to) ->
- PaymentPersistence.repartition frequency search from (Calendar.addDays 1 to)
- Nothing ->
- return M.empty
-
- (preIncomeRepartition, postIncomeRepartition) <-
- PaymentPersistence.getPreAndPostPaymentRepartition paymentRange users
-
- let exceedingPayers = Payer.getExceedingPayers users cumulativeIncome preIncomeRepartition postIncomeRepartition
-
- header = PaymentHeader
- { _paymentHeader_exceedingPayers = exceedingPayers
- , _paymentHeader_repartition = searchRepartition
- }
-
- return $ PaymentPage page frequency header payments count) >>= S.json
- )
-
-create :: CreatePaymentForm -> ActionM ()
-create form =
- Secure.loggedAction (\user ->
- (liftIO . Query.run $ do
- cs <- map _category_id <$> CategoryPersistence.listAll
- case PaymentValidation.createPayment cs form of
- Success (CreatePayment name cost date category frequency) ->
- Right <$> PaymentPersistence.create (_user_id user) name cost date category frequency
- Failure validationError ->
- return $ Left validationError
- ) >>= ControllerHelper.okOrBadRequest
- )
-
-edit :: EditPaymentForm -> ActionM ()
-edit form =
- Secure.loggedAction (\user ->
- (liftIO . Query.run $ do
- cs <- map _category_id <$> CategoryPersistence.listAll
- case PaymentValidation.editPayment cs form of
- Success (EditPayment paymentId name cost date category frequency) -> do
- isSuccess <- PaymentPersistence.edit (_user_id user) paymentId name cost date category frequency
- return $ if isSuccess then
- Right ()
- else
- Left $ Msg.get Msg.Error_PaymentEdit
- Failure validationError ->
- return $ Left validationError
- ) >>= ControllerHelper.okOrBadRequest
- )
-
-delete :: PaymentId -> ActionM ()
-delete paymentId =
- Secure.loggedAction (\user ->
- liftIO . Query.run $ PaymentPersistence.delete (_user_id user) paymentId
- )
-
-searchCategory :: Text -> ActionM ()
-searchCategory paymentName =
- Secure.loggedAction (\_ -> do
- (liftIO $ Query.run (PaymentPersistence.searchCategory paymentName))
- >>= S.json
- )
diff --git a/server/src/Controller/Statistics.hs b/server/src/Controller/Statistics.hs
deleted file mode 100644
index 500c93c..0000000
--- a/server/src/Controller/Statistics.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Controller.Statistics
- ( paymentsAndIncomes
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import Web.Scotty (ActionM)
-import qualified Web.Scotty as S
-
-import qualified Model.Query as Query
-import qualified Persistence.Income as IncomePersistence
-import qualified Persistence.Payment as PaymentPersistence
-import qualified Secure
-import qualified Statistics
-
-paymentsAndIncomes :: ActionM ()
-paymentsAndIncomes =
- Secure.loggedAction (\_ -> do
- payments <- liftIO $ Query.run PaymentPersistence.listAllPunctual
- incomes <- liftIO $ Query.run IncomePersistence.listAll
- S.json (Statistics.paymentsAndIncomes payments incomes)
- )
diff --git a/server/src/Controller/User.hs b/server/src/Controller/User.hs
deleted file mode 100644
index a7bb136..0000000
--- a/server/src/Controller/User.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-module Controller.User
- ( list
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import Web.Scotty (ActionM)
-import qualified Web.Scotty as S
-
-import qualified Model.Query as Query
-import qualified Persistence.User as UserPersistence
-import qualified Secure
-
-list :: ActionM ()
-list =
- Secure.loggedAction (\_ ->
- (liftIO . Query.run $ UserPersistence.list) >>= S.json
- )
diff --git a/server/src/Cookie.hs b/server/src/Cookie.hs
deleted file mode 100644
index 00d73f2..0000000
--- a/server/src/Cookie.hs
+++ /dev/null
@@ -1,55 +0,0 @@
-module Cookie
- ( makeSimpleCookie
- , setCookie
- , setSimpleCookie
- , getCookie
- , getCookies
- , deleteCookie
- ) where
-
-import Control.Monad (liftM)
-
-import qualified Data.Text as TS
-import qualified Data.Text.Encoding as TS
-import qualified Data.Text.Lazy.Encoding as TL
-
-import Conf (Conf)
-import qualified Conf
-
-import qualified Data.Map as Map
-
-import qualified Data.ByteString.Lazy as BSL
-
-import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
-
-import Blaze.ByteString.Builder (toLazyByteString)
-
-import Web.Cookie
-import Web.Scotty.Trans
-
-makeSimpleCookie :: Conf -> TS.Text -> TS.Text -> SetCookie
-makeSimpleCookie conf name value =
- def
- { setCookieName = TS.encodeUtf8 name
- , setCookieValue = TS.encodeUtf8 value
- , setCookiePath = Just $ TS.encodeUtf8 "/"
- , setCookieSecure = Conf.https conf
- , setCookieHttpOnly = True
- }
-
-setCookie :: (Monad m) => SetCookie -> ActionT e m ()
-setCookie name = addHeader "Set-Cookie" (TL.decodeUtf8 . toLazyByteString $ renderSetCookie name)
-
-setSimpleCookie :: (Monad m) => Conf -> TS.Text -> TS.Text -> ActionT e m ()
-setSimpleCookie conf name value = setCookie $ makeSimpleCookie conf name value
-
-getCookie :: (Monad m, ScottyError e) => TS.Text -> ActionT e m (Maybe TS.Text)
-getCookie name = liftM (Map.lookup name) getCookies
-
-getCookies :: (Monad m, ScottyError e) => ActionT e m (Map.Map TS.Text TS.Text)
-getCookies =
- liftM (Map.fromList . maybe [] parse) $ header "Cookie"
- where parse = parseCookiesText . BSL.toStrict . TL.encodeUtf8
-
-deleteCookie :: (Monad m) => Conf -> TS.Text -> ActionT e m ()
-deleteCookie conf name = setCookie $ (makeSimpleCookie conf name "") { setCookieExpires = Just $ posixSecondsToUTCTime 0 }
diff --git a/server/src/Design/Appearing.hs b/server/src/Design/Appearing.hs
deleted file mode 100644
index 79b94b3..0000000
--- a/server/src/Design/Appearing.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-module Design.Appearing
- ( design
- ) where
-
-import Clay
-
-design :: Css
-design = do
-
- appearKeyframe
-
- ".g-Appearing" ? do
- appearAnimation
-
-appearAnimation :: Css
-appearAnimation = do
- animationName "appear"
- animationDuration (sec 0.2)
- animationTimingFunction easeIn
-
-appearKeyframe :: Css
-appearKeyframe = keyframes
- "appear"
- [ (0, "opacity" -: "0")
- ]
diff --git a/server/src/Design/Color.hs b/server/src/Design/Color.hs
deleted file mode 100644
index e7f5aec..0000000
--- a/server/src/Design/Color.hs
+++ /dev/null
@@ -1,40 +0,0 @@
-module Design.Color where
-
-import Clay
-import qualified Clay.Color as C
-import Data.Text (Text)
-
--- http://chir.ag/projects/name-that-color/#969696
-
-white :: C.Color
-white = C.white
-
-black :: C.Color
-black = C.black
-
-chestnutRose :: C.Color
-chestnutRose = C.rgb 207 92 86
-
-unknown :: C.Color
-unknown = C.rgb 86 92 207
-
-mossGreen :: C.Color
-mossGreen = C.rgb 159 210 165
-
-gothic :: C.Color
-gothic = C.rgb 108 162 164
-
-negroni :: C.Color
-negroni = C.rgb 255 223 196
-
-wildSand :: C.Color
-wildSand = C.rgb 245 245 245
-
-silver :: C.Color
-silver = C.rgb 200 200 200
-
-dustyGray :: C.Color
-dustyGray = C.rgb 150 150 150
-
-toString :: C.Color -> Text
-toString = plain . unValue . value
diff --git a/server/src/Design/Constants.hs b/server/src/Design/Constants.hs
deleted file mode 100644
index a3123d9..0000000
--- a/server/src/Design/Constants.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Design.Constants where
-
-import Clay
-
-iconFontSize :: Size LengthUnit
-iconFontSize = px 32
-
-radius :: Size LengthUnit
-radius = px 3
-
-blockPadding :: Size LengthUnit
-blockPadding = px 15
-
-blockPercentWidth :: Double
-blockPercentWidth = 90
-
-blockPercentMargin :: Double
-blockPercentMargin = (100 - blockPercentWidth) / 2
-
-inputHeight :: Double
-inputHeight = 40
-
-focusLighten :: Color -> Color
-focusLighten baseColor = baseColor +. 20
-
-focusDarken :: Color -> Color
-focusDarken baseColor = baseColor -. 20
diff --git a/server/src/Design/Errors.hs b/server/src/Design/Errors.hs
deleted file mode 100644
index 9f435eb..0000000
--- a/server/src/Design/Errors.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-module Design.Errors
- ( design
- ) where
-
-import Clay
-
-import Design.Color as Color
-
-design :: Css
-design = do
- position fixed
- top (px 20)
- left (pct 50)
- "transform" -: "translateX(-50%)"
- margin (px 0) (px 0) (px 0) (px 0)
- disapearKeyframes
-
- ".error" ? do
- disapearAnimation
- let errorColor = Color.chestnutRose -. 15
- color errorColor
- border solid (px 2) errorColor
- backgroundColor Color.white
- borderRadius (px 5) (px 5) (px 5) (px 5)
- padding (px 5) (px 5) (px 5) (px 5)
-
- before & display none
-
-disapearAnimation :: Css
-disapearAnimation = do
- animationName "disapear"
- animationDelay (sec 5)
- animationDuration (sec 1)
- animationFillMode forwards
-
-disapearKeyframes :: Css
-disapearKeyframes = keyframes
- "disapear"
- [ ( 10
- , do
- opacity 0
- height (px 40)
- lineHeight (px 40)
- marginBottom (px 10)
- )
- , ( 100
- , do
- opacity 0
- height (px 0)
- lineHeight (px 0)
- marginBottom (px 0)
- )
- ]
diff --git a/server/src/Design/Form.hs b/server/src/Design/Form.hs
deleted file mode 100644
index 5713bfe..0000000
--- a/server/src/Design/Form.hs
+++ /dev/null
@@ -1,101 +0,0 @@
-module Design.Form
- ( design
- ) where
-
-import Data.Monoid ((<>))
-
-import Clay
-
-import Design.Color as Color
-
-design :: Css
-design = do
-
- let inputHeight = 30
- let inputTop = 22
- let inputPaddingBottom = 3
-
- ".textInput" ? do
- position relative
- marginBottom (em 2)
- paddingTop (px inputTop)
- marginTop (px (-10))
-
- input ? do
- width (pct 100)
- position relative
- backgroundColor transparent
- paddingBottom (px inputPaddingBottom)
- paddingRight (px 14) -- Space for the delete icon
- borderStyle none
- borderBottom solid (px 1) Color.dustyGray
- marginBottom (px 5)
- height (px inputHeight)
- lineHeight (px inputHeight)
- focus & do
- borderWidth (px 2)
- paddingBottom (px $ inputPaddingBottom - 1)
-
- ".label" ? do
- zIndex (-1)
- color Color.silver
- lineHeight (px inputHeight)
- position absolute
- top (px inputTop)
- left (px 0)
- transition "all" (sec 0.2) easeInOut (sec 0)
-
- button ? do
- position absolute
- right (px 0)
- top (px 27)
- svg ? "path" ?
- ("fill" -: Color.toString Color.silver)
- hover & svg ? "path" ?
- ("fill" -: Color.toString (Color.silver -. 25))
-
- (input # ".filled" |+ ".label") <> (input # focus |+ ".label") ? do
- top (px 0)
- fontSize (pct 80)
-
- ".error" & do
- input ? do
- borderBottomColor Color.chestnutRose
-
- ".errorMessage" ? do
- position absolute
- color Color.chestnutRose
- fontSize (pct 80)
-
- ".colorInput" ? do
- display flex
- alignItems center
- marginBottom (em 1.5)
-
- input ? do
- borderColor transparent
- backgroundColor transparent
-
- ".selectInput" ? do
-
- ".label" ? do
- color Color.silver
- display block
- marginBottom (px 10)
- fontSize (pct 80)
-
- select ? do
- width (pct 100)
- backgroundColor Color.white
- border solid (px 1) Color.silver
- sym borderRadius (px 3)
- sym2 padding (px 5) (px 8)
- option ? sym2 padding (px 5) (px 8)
- focus & backgroundColor Color.wildSand
-
- ".error" & do
- select ? borderColor Color.chestnutRose
- ".errorMessage" ? do
- color Color.chestnutRose
- fontSize (pct 80)
- marginTop (em 0.5)
diff --git a/server/src/Design/Global.hs b/server/src/Design/Global.hs
deleted file mode 100644
index c67db7c..0000000
--- a/server/src/Design/Global.hs
+++ /dev/null
@@ -1,165 +0,0 @@
-module Design.Global
- ( globalDesign
- ) where
-
-import Clay
-import Clay.Color as C
-import Data.Text.Lazy (Text)
-
-import qualified Design.Appearing as Appearing
-import qualified Design.Color as Color
-import qualified Design.Constants as Constants
-import qualified Design.Errors as Errors
-import qualified Design.Form as Form
-import qualified Design.Helper as Helper
-import qualified Design.Loadable as Loadable
-import qualified Design.Media as Media
-import qualified Design.Modal as Modal
-import qualified Design.Tooltip as Tooltip
-import qualified Design.Views as Views
-
-globalDesign :: Text
-globalDesign = renderWith compact [] global
-
-global :: Css
-global = do
- ".errors" ? Errors.design
- Appearing.design
- Modal.design
- ".tooltip" ? Tooltip.design
- Views.design
- Form.design
- Loadable.design
-
- spinKeyframes
- appearKeyframe
-
- html ? do
- height (pct 100)
-
- "g-Body--Modal" ?
- overflowY hidden
-
- body ? do
- position relative
- minWidth (px 320)
- height (pct 100)
- fontFamily ["Cantarell"] [sansSerif]
- Media.tablet $ do
- fontSize (px 15)
- button ? fontSize (px 15)
- input ? fontSize (px 15)
- Media.mobile $ do
- fontSize (px 14)
- button ? fontSize (px 14)
- input ? fontSize (px 14)
-
- ".app" ? do
- appearAnimation
- display flex
- height (pct 100)
- flexDirection column
-
- -- "main" ?
- -- appearAnimation
-
- ".pageSpinner" ? do
- display flex
- alignItems center
- justifyContent center
- flexGrow 1
-
- ".spinner" ? do
- display flex
- alignItems center
- justifyContent center
- width (pct 100)
- height (pct 100)
- paddingBottom (pct 10)
-
- before & do
- display block
- content (stringContent "")
- width (px 50)
- height (px 50)
- border solid (px 3) (C.setA 0.3 Color.chestnutRose)
- sym borderRadius (pct 50)
- borderTopColor Color.chestnutRose
- spinKeyframes
- spinAnimation
-
- a ? cursor pointer
-
- input ? fontSize inherit
-
- h1 ? do
- color Color.chestnutRose
- lineHeight (em 1.3)
-
- Media.desktop $ fontSize (px 24)
- Media.tablet $ fontSize (px 22)
- Media.mobile $ fontSize (px 20)
-
- ul ? do
- "margin-top" -: "1vh"
- "margin-bottom" -: "3vh"
- "margin-left" -: "1vh"
- li <? do
- "margin-bottom" -: "2vh"
- before & do
- content (stringContent "• ")
- color Color.chestnutRose
- "margin-right" -: "0.3vw"
- ul <? do
- "margin-left" -: "3vh"
- "margin-top" -: "2vh"
-
- ".dialog" ? ".content" ? button ? do
- ".confirm" & Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten
- ".undo" & Helper.button Color.silver Color.white (px Constants.inputHeight) Constants.focusLighten
-
- svg ? height (pct 100)
-
- button ? do
- position relative
-
- ".content" ? do
- display flex
-
- svg # ".loader" ? do
- display none
- position absolute
-
- ".waiting" & do
- ".content" ? do
- opacity 0
- svg # ".loader" ? do
- display block
- spinAnimation
-
- select ? cursor pointer
-
-spinAnimation :: Css
-spinAnimation = do
- animationName "rotate"
- animationDuration (sec 1)
- animationTimingFunction easeInOut
- animationIterationCount infinite
-
-spinKeyframes :: Css
-spinKeyframes = keyframes
- "rotate"
- [ (100, "transform" -: "rotate(360deg)")
- ]
-
-appearAnimation :: Css
-appearAnimation = do
- animationName "appear"
- animationDuration (sec 0.2)
- animationTimingFunction easeIn
-
-appearKeyframe :: Css
-appearKeyframe = keyframes
- "appear"
- [ (0, "opacity" -: "0")
- ]
diff --git a/server/src/Design/Helper.hs b/server/src/Design/Helper.hs
deleted file mode 100644
index e586d56..0000000
--- a/server/src/Design/Helper.hs
+++ /dev/null
@@ -1,48 +0,0 @@
-module Design.Helper
- ( clearFix
- , button
- , centeredWithMargin
- , verticalCentering
- ) where
-
-import Prelude hiding (span)
-
-import Clay hiding (button)
-
-import Design.Constants
-
-clearFix :: Css
-clearFix =
- after & do
- content (stringContent "")
- display displayTable
- clear both
-
-button :: Color -> Color -> Size a -> (Color -> Color) -> Css
-button backgroundCol textCol h focusOp = do
- display flex
- alignItems center
- justifyContent center
- backgroundColor backgroundCol
- padding (px 0) (px 10) (px 0) (px 10)
- color textCol
- borderRadius radius radius radius radius
- verticalAlign middle
- cursor pointer
- lineHeight h
- height h
- textAlign (alignSide sideCenter)
- hover & backgroundColor (focusOp backgroundCol)
- focus & backgroundColor (focusOp backgroundCol)
-
-centeredWithMargin :: Css
-centeredWithMargin = do
- width (pct blockPercentWidth)
- marginLeft auto
- marginRight auto
-
-verticalCentering :: Css
-verticalCentering = do
- position absolute
- top (pct 50)
- "transform" -: "translateY(-50%)"
diff --git a/server/src/Design/Loadable.hs b/server/src/Design/Loadable.hs
deleted file mode 100644
index 6b13f2d..0000000
--- a/server/src/Design/Loadable.hs
+++ /dev/null
@@ -1,29 +0,0 @@
-module Design.Loadable
- ( design
- ) where
-
-import Clay
-
-design :: Css
-design = do
- ".g-Loadable" ? do
- position relative
- width (pct 100)
- height (pct 100)
-
- ".g-Loadable__Spinner" ? do
- position absolute
- top (px 0)
- left (px 0)
- width (pct 100)
- height (pct 100)
- display none
-
- ".g-Loadable__Spinner--Loading" ? do
- display block
-
- ".g-Loadable__Content" ?
- transition "opacity" (sec 0.4) ease (sec 0)
-
- ".g-Loadable__Content--Loading" ?
- opacity 0.5
diff --git a/server/src/Design/Media.hs b/server/src/Design/Media.hs
deleted file mode 100644
index 19a3b8c..0000000
--- a/server/src/Design/Media.hs
+++ /dev/null
@@ -1,36 +0,0 @@
-module Design.Media
- ( mobile
- , mobileTablet
- , tablet
- , tabletDesktop
- , desktop
- ) where
-
-import Clay hiding (query)
-import qualified Clay
-import qualified Clay.Media as Media
-import Clay.Stylesheet (Feature)
-
-mobile :: Css -> Css
-mobile = query [Media.maxWidth mobileTabletLimit]
-
-mobileTablet :: Css -> Css
-mobileTablet = query [Media.maxWidth tabletDesktopLimit]
-
-tablet :: Css -> Css
-tablet = query [Media.minWidth mobileTabletLimit, Media.maxWidth tabletDesktopLimit]
-
-tabletDesktop :: Css -> Css
-tabletDesktop = query [Media.minWidth mobileTabletLimit]
-
-desktop :: Css -> Css
-desktop = query [Media.minWidth tabletDesktopLimit]
-
-query :: [Feature] -> Css -> Css
-query = Clay.query Media.screen
-
-mobileTabletLimit :: Size LengthUnit
-mobileTabletLimit = (px 520)
-
-tabletDesktopLimit :: Size LengthUnit
-tabletDesktopLimit = (px 950)
diff --git a/server/src/Design/Modal.hs b/server/src/Design/Modal.hs
deleted file mode 100644
index 1195e10..0000000
--- a/server/src/Design/Modal.hs
+++ /dev/null
@@ -1,69 +0,0 @@
-module Design.Modal
- ( design
- ) where
-
-import Clay
-import Data.Monoid ((<>))
-
-import qualified Design.View.Payment.Form as Form
-
-design :: Css
-design = do
-
- appearKeyframe
-
- ".g-Modal" ? do
- display none
- appearAnimation
- transition "all" (sec 0.2) ease (sec 0)
- opacity 0
-
- ".g-Modal--Show" & do
- display block
- opacity 1
-
- ".g-Modal--Hiding" & do
- display block
-
- ".g-Modal__Curtain" ? do
- position fixed
- top (px 0)
- left (px 0)
- width (pct 100)
- height (pct 100)
- backgroundColor (rgba 0 0 0 0.6)
- zIndex 1
-
- ".g-Modal__Content" ? do
- minWidth (px 300)
- position fixed
- top (pct 25)
- left (pct 50)
- "transform" -: "translate(-50%, -25%)"
- zIndex 1
- backgroundColor white
- sym borderRadius (px 5)
- boxShadow . pure . bsColor (rgba 0 0 0 0.5) $ shadowWithBlur (px 0) (px 0) (px 15)
-
- ".form" ? Form.design
-
- ".paymentModal" & do
- ".radioGroup" ? ".title" ? display none
- ".selectInput" ? do
- select ? width (pct 100)
- marginBottom (em 1)
-
- ".deletePaymentModal" <> ".deleteIncomeModal" ? do
- h1 ? marginBottom (em 1.5)
-
-appearAnimation :: Css
-appearAnimation = do
- animationName "appear"
- animationDuration (sec 0.15)
- animationTimingFunction easeIn
-
-appearKeyframe :: Css
-appearKeyframe = keyframes
- "appear"
- [ (0, "opacity" -: "0")
- ]
diff --git a/server/src/Design/Tooltip.hs b/server/src/Design/Tooltip.hs
deleted file mode 100644
index eef804e..0000000
--- a/server/src/Design/Tooltip.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Design.Tooltip
- ( design
- ) where
-
-import Clay
-
-import Design.Color as Color
-
-design :: Css
-design = do
- backgroundColor Color.mossGreen
- borderRadius (px 5) (px 5) (px 5) (px 5)
- padding (px 5) (px 5) (px 5) (px 5)
- color Color.white
diff --git a/server/src/Design/View/ConfirmDialog.hs b/server/src/Design/View/ConfirmDialog.hs
deleted file mode 100644
index 410d4d8..0000000
--- a/server/src/Design/View/ConfirmDialog.hs
+++ /dev/null
@@ -1,36 +0,0 @@
-module Design.View.ConfirmDialog
- ( design
- ) where
-
-import Clay
-
-import qualified Design.Color as Color
-import qualified Design.Constants as Constants
-import qualified Design.Helper as Helper
-
-design :: Css
-design = do
- ".confirm" ? do
- ".confirmHeader" ? do
- backgroundColor Color.chestnutRose
- fontSize (px 18)
- color Color.white
- sym padding (px 20)
- textAlign (alignSide sideCenter)
- borderRadius (px 5) (px 5) (px 0) (px 0)
-
- ".confirmContent" ? do
- sym padding (px 20)
-
- ".buttons" ? do
- display flex
- justifyContent spaceAround
- marginTop (em 1.5)
-
- ".confirm" ?
- Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten
- ".undo" ?
- Helper.button Color.silver Color.white (px Constants.inputHeight) Constants.focusLighten
-
- (".confirm" <> ".undo") ?
- width (px 90)
diff --git a/server/src/Design/View/Header.hs b/server/src/Design/View/Header.hs
deleted file mode 100644
index 2ad0455..0000000
--- a/server/src/Design/View/Header.hs
+++ /dev/null
@@ -1,93 +0,0 @@
-module Design.View.Header
- ( design
- ) where
-
-import Data.Monoid ((<>))
-
-import Clay
-
-import Design.Color as Color
-import qualified Design.Media as Media
-
-desktopLineHeight :: Double
-desktopLineHeight = 80
-
-tabletLineHeight :: Double
-tabletLineHeight = 60
-
-mobileLineHeight :: Double
-mobileLineHeight = 40
-
-design :: Css
-design = do
- display flex
- "flex-wrap" -: "wrap"
- position relative
- backgroundColor Color.chestnutRose
- color Color.white
-
- Media.desktop $ do
- minHeight (px desktopLineHeight)
- lineHeight (px desktopLineHeight)
- marginBottom (em 3)
- Media.tablet $ do
- minHeight (px (tabletLineHeight * 2))
- lineHeight (px tabletLineHeight)
- marginBottom (em 2)
- Media.mobile $ do
- minHeight (px (mobileLineHeight * 2))
- lineHeight (px mobileLineHeight)
- marginBottom (em 1.5)
-
- ".title" <> ".item" ? do
- Media.tabletDesktop $ sym2 padding (px 0) (px 20)
- Media.mobile $ sym2 padding (px 0) (px 10)
-
- ".title" ? do
- textAlign (alignSide sideLeft)
-
- Media.desktop $ do
- fontSize (px 35)
- display inlineBlock
- Media.tablet $ do
- fontSize (px 28)
- display inlineBlock
- width (pct 100)
- Media.mobile $ do
- fontSize (px 22)
- width (pct 100)
-
- ".item" ? do
- display inlineBlock
- transition "background-color" (ms 50) easeIn (sec 0)
- ".current" & backgroundColor (Color.chestnutRose -. 20)
- Media.mobile $ fontSize (px 13)
-
- (".item" # hover) <> (".item" # focus) ?
- backgroundColor (Color.chestnutRose +. 10)
-
- (".item.current" # hover) <> (".item.current" # focus) ?
- backgroundColor (Color.chestnutRose -. 10)
-
- ".nameSignOut" ? do
- display flex
- position absolute
- top (px 0)
- right (px 0)
-
- Media.desktop $ height (px desktopLineHeight)
- Media.tablet $ height (px tabletLineHeight)
- Media.mobile $ height (px mobileLineHeight)
-
- ".name" ? do
- Media.mobile $ display none
- Media.tabletDesktop $ sym2 padding (px 0) (px 20)
-
- ".signOut" ? do
- display flex
- justifyContent center
- alignItems center
- svg ? do
- Media.tabletDesktop $ width (px 30)
- Media.mobile $ width (px 20)
- "path" ? ("fill" -: "white")
diff --git a/server/src/Design/View/NotFound.hs b/server/src/Design/View/NotFound.hs
deleted file mode 100644
index 150c6fc..0000000
--- a/server/src/Design/View/NotFound.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Design.View.NotFound
- ( design
- ) where
-
-import Clay
-import Prelude hiding (rem)
-
-import qualified Design.Color as Color
-
-design :: Css
-design = do
-
- marginLeft (rem 3)
-
- ".link" ? do
- display block
- marginTop (rem 1)
- color Color.chestnutRose
- textDecoration underline
- hover &
- color (Color.chestnutRose +. 15)
diff --git a/server/src/Design/View/Pages.hs b/server/src/Design/View/Pages.hs
deleted file mode 100644
index 1482ef4..0000000
--- a/server/src/Design/View/Pages.hs
+++ /dev/null
@@ -1,55 +0,0 @@
-module Design.View.Pages
- ( design
- ) where
-
-import Clay
-
-import qualified Design.Color as Color
-import qualified Design.Constants as Constants
-import qualified Design.Helper as Helper
-import qualified Design.Media as Media
-
-design :: Css
-design =
- ".pages" ? do
- display flex
- justifyContent center
-
- Media.desktop $ do
- padding (px 40) (px 30) (px 30) (px 30)
-
- Media.tablet $ do
- padding (px 30) (px 30) (px 30) (px 30)
-
- Media.mobile $ do
- padding (px 20) (px 0) (px 20) (px 0)
- lineHeight (px 40)
-
- svg ? "path" ? ("fill" -: Color.toString Color.dustyGray)
-
- ".page" ? do
- display inlineBlock
- fontWeight bold
-
- Media.desktop $ do
- Helper.button Color.white Color.dustyGray (px 50) Constants.focusDarken
-
- Media.tabletDesktop $ do
- border solid (px 2) Color.dustyGray
- marginRight (px 10)
-
- Media.tablet $ do
- Helper.button Color.white Color.dustyGray (px 40) Constants.focusDarken
- fontSize (px 15)
-
- Media.mobile $ do
- Helper.button Color.white Color.dustyGray (px 30) Constants.focusDarken
- fontSize (px 12)
- border solid (px 1) Color.dustyGray
- marginRight (px 5)
-
- ":not(.current)" & cursor pointer
-
- ".current" & do
- borderColor Color.chestnutRose
- color Color.chestnutRose
diff --git a/server/src/Design/View/Payment.hs b/server/src/Design/View/Payment.hs
deleted file mode 100644
index 94e4f85..0000000
--- a/server/src/Design/View/Payment.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module Design.View.Payment
- ( design
- ) where
-
-import Clay
-
-import qualified Design.Color as Color
-import qualified Design.View.Payment.HeaderForm as HeaderForm
-import qualified Design.View.Payment.HeaderInfos as HeaderInfos
-
-design :: Css
-design = do
- HeaderForm.design
- HeaderInfos.design
- ".g-Payment__Refund" ? color Color.mossGreen
diff --git a/server/src/Design/View/Payment/Add.hs b/server/src/Design/View/Payment/Add.hs
deleted file mode 100644
index 5ecae7a..0000000
--- a/server/src/Design/View/Payment/Add.hs
+++ /dev/null
@@ -1,35 +0,0 @@
-module Design.View.Payment.Add
- ( design
- ) where
-
-import Clay
-
-import qualified Design.Color as Color
-import qualified Design.Constants as Constants
-import qualified Design.Helper as Helper
-
-design :: Css
-design = do
- ".addHeader" ? do
- backgroundColor Color.chestnutRose
- fontSize (px 18)
- color Color.white
- sym2 padding (px 20) (px 30)
- textAlign (alignSide sideCenter)
- borderRadius (px 5) (px 5) (px 0) (px 0)
-
- ".addContent" ? do
- sym2 padding (px 20) (px 30)
-
- ".buttons" ? do
- display flex
- justifyContent spaceAround
- marginTop (em 1.5)
-
- ".confirm" ?
- Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten
- ".undo" ?
- Helper.button Color.silver Color.white (px Constants.inputHeight) Constants.focusLighten
-
- (".confirm" <> ".undo") ?
- width (px 90)
diff --git a/server/src/Design/View/Payment/Form.hs b/server/src/Design/View/Payment/Form.hs
deleted file mode 100644
index aada12b..0000000
--- a/server/src/Design/View/Payment/Form.hs
+++ /dev/null
@@ -1,35 +0,0 @@
-module Design.View.Payment.Form
- ( design
- ) where
-
-import Clay
-
-import qualified Design.Color as Color
-import qualified Design.Constants as Constants
-import qualified Design.Helper as Helper
-
-design :: Css
-design = do
- ".formHeader" ? do
- backgroundColor Color.chestnutRose
- fontSize (px 18)
- color Color.white
- sym2 padding (px 20) (px 30)
- textAlign (alignSide sideCenter)
- borderRadius (px 5) (px 5) (px 0) (px 0)
-
- ".formContent" ? do
- sym2 padding (px 20) (px 30)
-
- ".buttons" ? do
- display flex
- justifyContent spaceAround
- marginTop (em 1.5)
-
- ".confirm" ?
- Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten
- ".undo" ?
- Helper.button Color.silver Color.white (px Constants.inputHeight) Constants.focusLighten
-
- (".confirm" <> ".undo") ?
- width (px 90)
diff --git a/server/src/Design/View/Payment/HeaderForm.hs b/server/src/Design/View/Payment/HeaderForm.hs
deleted file mode 100644
index 6081443..0000000
--- a/server/src/Design/View/Payment/HeaderForm.hs
+++ /dev/null
@@ -1,40 +0,0 @@
-module Design.View.Payment.HeaderForm
- ( design
- ) where
-
-import Clay
-
-import qualified Design.Color as Color
-import qualified Design.Constants as Constants
-import qualified Design.Helper as Helper
-import qualified Design.Media as Media
-
-design :: Css
-design = do
-
- ".g-PaymentHeaderForm" ? do
- marginBottom (em 2)
- marginLeft (pct Constants.blockPercentMargin)
- marginRight (pct Constants.blockPercentMargin)
- display flex
- justifyContent spaceBetween
- alignItems center
- Media.mobile $ flexDirection column
-
- ".textInput" ? do
- display inlineBlock
- marginBottom (px 0)
-
- Media.tabletDesktop $ marginRight (px 30)
- Media.mobile $ do
- marginBottom (em 1)
- width (pct 100)
-
- ".selectInput" ? do
- Media.tabletDesktop $ display inlineBlock
- Media.mobile $ marginBottom (em 2)
-
- ".addPayment" ? do
- Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten
- Media.mobile $ width (pct 100)
- flexShrink 0
diff --git a/server/src/Design/View/Payment/HeaderInfos.hs b/server/src/Design/View/Payment/HeaderInfos.hs
deleted file mode 100644
index acb393b..0000000
--- a/server/src/Design/View/Payment/HeaderInfos.hs
+++ /dev/null
@@ -1,50 +0,0 @@
-module Design.View.Payment.HeaderInfos
- ( design
- ) where
-
-import Data.Monoid ((<>))
-
-import Clay
-
-import qualified Design.Color as Color
-import qualified Design.Constants as Constants
-import qualified Design.Media as Media
-
-design :: Css
-design = do
-
- ".g-PaymentHeaderInfos" ? do
- Media.desktop $ marginBottom (em 2)
- Media.mobileTablet $ marginBottom (em 1)
- marginLeft (pct Constants.blockPercentMargin)
- marginRight (pct Constants.blockPercentMargin)
-
- ".g-PaymentHeaderInfos__ExceedingPayers" ? do
- backgroundColor Color.mossGreen
- borderRadius (px 5) (px 5) (px 5) (px 5)
- color Color.white
- lineHeight (px Constants.inputHeight)
- paddingLeft (px 10)
- paddingRight (px 10)
- marginBottom (em 1)
-
- Media.mobile $ do
- textAlign (alignSide sideCenter)
-
- ".exceedingPayer:not(:last-child)::after" ? content (stringContent ", ")
-
- ".userName" ? marginRight (px 8)
-
- ".g-PaymentHeaderInfos__Repartition" ? do
- Media.tabletDesktop $ lineHeight (px Constants.inputHeight)
- Media.mobile $ lineHeight (px 25)
-
- ".total" <> ".partition" ? do
- Media.mobileTablet $ display block
- Media.mobile $ do
- fontSize (pct 90)
- textAlign (alignSide sideCenter)
-
- ".partition" ? do
- color Color.dustyGray
- Media.desktop $ marginLeft (px 15)
diff --git a/server/src/Design/View/SignIn.hs b/server/src/Design/View/SignIn.hs
deleted file mode 100644
index 42c9621..0000000
--- a/server/src/Design/View/SignIn.hs
+++ /dev/null
@@ -1,36 +0,0 @@
-module Design.View.SignIn
- ( design
- ) where
-
-import Clay
-import Data.Monoid ((<>))
-import Prelude hiding (rem)
-
-import qualified Design.Color as Color
-import qualified Design.Constants as Constants
-import qualified Design.Helper as Helper
-
-design :: Css
-design = do
- let inputHeight = 50
- width (px 350)
- sym2 padding (rem 0) (rem 2)
- marginTop (px 100)
- marginLeft auto
- marginRight auto
-
- button # ".validate" ? do
- Helper.button Color.gothic Color.white (px inputHeight) Constants.focusLighten
- display flex
- alignItems center
- justifyContent center
- width (pct 100)
- fontSize (em 1.2)
- svg ? "path" ? ("fill" -: "white")
-
- ".success" <> ".error" ? do
- marginTop (px 40)
- textAlign (alignSide sideCenter)
-
- ".success" ? color Color.mossGreen
- ".error" ? color Color.chestnutRose
diff --git a/server/src/Design/View/Stat.hs b/server/src/Design/View/Stat.hs
deleted file mode 100644
index 2e4ecad..0000000
--- a/server/src/Design/View/Stat.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-module Design.View.Stat
- ( design
- ) where
-
-import Clay
-
-design :: Css
-design = do
- h1 ? paddingBottom (px 0)
-
- ".exceedingPayers" ? ".userName" ? marginRight (px 5)
-
- ".mean" ? marginBottom (em 1.5)
-
- ".g-Chart" ? do
- width (pct 75)
- sym2 margin (px 0) auto
diff --git a/server/src/Design/View/Table.hs b/server/src/Design/View/Table.hs
deleted file mode 100644
index 56bd389..0000000
--- a/server/src/Design/View/Table.hs
+++ /dev/null
@@ -1,99 +0,0 @@
-module Design.View.Table
- ( design
- ) where
-
-import Data.Monoid ((<>))
-
-import Clay
-
-import Design.Color as Color
-import qualified Design.Media as Media
-
-design :: Css
-design = do
- ".emptyTableMsg" ? do
- margin (em 2) (em 2) (em 2) (em 2)
- textAlign (alignSide sideCenter)
-
- ".table" ? do
- minHeight (px 540)
-
- ".lines" ? do
- Media.tabletDesktop $ display displayTable
- width (pct 100)
- textAlign (alignSide (sideCenter))
-
- ".header" <> ".row" ? do
- Media.tabletDesktop $ display tableRow
-
- ".header" ? do
- Media.desktop $ do
- fontSize (px 18)
- height (px 70)
-
- Media.tabletDesktop $ do
- backgroundColor Color.gothic
- color Color.white
-
- Media.tablet $ do
- fontSize (px 16)
- height (px 60)
-
- Media.mobile $ do
- display none
-
- ".row" ? do
- nthChild "even" & backgroundColor Color.wildSand
-
- Media.desktop $ do
- fontSize (px 18)
- height (px 60)
-
- Media.tablet $ do
- height (px 50)
-
- Media.mobile $ do
- lineHeight (px 25)
- paddingTop (px 10)
- paddingBottom (px 10)
-
- ".cell" ? do
- Media.tabletDesktop $ display tableCell
- position relative
- verticalAlign middle
-
- firstChild & do
- Media.mobile $ do
- fontSize (px 20)
- lineHeight (px 30)
- color Color.gothic
-
- ".refund" & color Color.mossGreen
-
- Media.desktop $ do
- ".shortDate" ? display none
- ".longDate" ? display inline
- Media.tablet $ do
- ".shortDate" ? display inline
- ".longDate" ? display none
- Media.mobile $ do
- ".shortDate" ? display none
- ".longDate" ? display inline
- marginBottom (em 0.5)
-
- ".cell.button" & do
- position relative
- textAlign (alignSide sideCenter)
- button ? do
- padding (px 10) (px 10) (px 10) (px 10)
- svg ? do
- "path" ? ("fill" -: Color.toString Color.chestnutRose)
- width (px 18)
- hover & "svg path" ? do
- "fill" -: "rgb(237, 122, 116)"
-
- Media.tabletDesktop $ width (pct 3)
-
- Media.mobile $ do
- display inlineBlock
- button ? display flex
diff --git a/server/src/Design/Views.hs b/server/src/Design/Views.hs
deleted file mode 100644
index 4552796..0000000
--- a/server/src/Design/Views.hs
+++ /dev/null
@@ -1,56 +0,0 @@
-module Design.Views
- ( design
- ) where
-
-import Clay
-
-import qualified Design.Color as Color
-import qualified Design.Constants as Constants
-import qualified Design.Helper as Helper
-import qualified Design.Media as Media
-import qualified Design.View.ConfirmDialog as ConfirmDialog
-import qualified Design.View.Header as Header
-import qualified Design.View.NotFound as NotFound
-import qualified Design.View.Pages as Pages
-import qualified Design.View.Payment as Payment
-import qualified Design.View.SignIn as SignIn
-import qualified Design.View.Stat as Stat
-import qualified Design.View.Table as Table
-
-design :: Css
-design = do
- header ? Header.design
- Payment.design
- ".signIn" ? SignIn.design
- Stat.design
- ".notfound" ? NotFound.design
- Table.design
- Pages.design
- ConfirmDialog.design
-
- ".withMargin" ? do
- "margin" -: "0 2vw"
-
- ".titleButton" ? do
- display flex
- marginBottom (em 1)
-
- Media.tabletDesktop $ do
- justifyContent spaceBetween
- alignItems center
-
- Media.mobile $ do
- flexDirection column
- "h1" ? marginBottom (em 0.5)
-
- button ? do
- Helper.button Color.chestnutRose Color.white (px Constants.inputHeight) Constants.focusLighten
- Media.mobile $ do
- width (pct 100)
- marginBottom (px 20)
-
- ".tag" ? do
- sym borderRadius (px 4)
- sym2 padding (px 2) (px 5)
- boxShadow . pure . bsColor (rgba 0 0 0 0.3) $ shadowWithBlur (px 2) (px 2) (px 5)
- color Color.white
diff --git a/server/src/Job/Daemon.hs b/server/src/Job/Daemon.hs
deleted file mode 100644
index d8cd522..0000000
--- a/server/src/Job/Daemon.hs
+++ /dev/null
@@ -1,37 +0,0 @@
-module Job.Daemon
- ( runDaemons
- ) where
-
-import Control.Concurrent (ThreadId, forkIO, threadDelay)
-import Control.Monad (forever)
-import Data.Time.Clock (UTCTime)
-
-import Conf (Conf)
-import Job.Frequency (Frequency (..), microSeconds)
-import Job.Kind (Kind (..))
-import Job.Model (actualizeLastCheck, actualizeLastExecution,
- getLastExecution)
-import Job.MonthlyPayment (monthlyPayment)
-import Job.WeeklyReport (weeklyReport)
-import qualified Model.Query as Query
-import Util.Time (belongToCurrentMonth, belongToCurrentWeek)
-
-runDaemons :: Conf -> IO ()
-runDaemons conf = do
- _ <- runDaemon MonthlyPayment EveryHour (fmap not . belongToCurrentMonth) monthlyPayment
- _ <- runDaemon WeeklyReport EveryHour (fmap not . belongToCurrentWeek) (weeklyReport conf)
- return ()
-
-runDaemon :: Kind -> Frequency -> (UTCTime -> IO Bool) -> (Maybe UTCTime -> IO UTCTime) -> IO ThreadId
-runDaemon kind frequency isLastExecutionTooOld runJob =
- forkIO . forever $ do
- mbLastExecution <- Query.run $ do
- actualizeLastCheck kind
- getLastExecution kind
- hasToRun <- case mbLastExecution of
- Just lastExecution -> isLastExecutionTooOld lastExecution
- Nothing -> return True
- if hasToRun
- then runJob mbLastExecution >>= (Query.run . actualizeLastExecution kind)
- else return ()
- threadDelay . microSeconds $ frequency
diff --git a/server/src/Job/Frequency.hs b/server/src/Job/Frequency.hs
deleted file mode 100644
index c5bef42..0000000
--- a/server/src/Job/Frequency.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module Job.Frequency
- ( Frequency(..)
- , microSeconds
- ) where
-
-data Frequency =
- EveryHour
- | EveryDay
- deriving (Eq, Read, Show)
-
-microSeconds :: Frequency -> Int
-microSeconds EveryHour = 1000000 * 60 * 60
-microSeconds EveryDay = (microSeconds EveryHour) * 24
diff --git a/server/src/Job/Kind.hs b/server/src/Job/Kind.hs
deleted file mode 100644
index 17997f7..0000000
--- a/server/src/Job/Kind.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-module Job.Kind
- ( Kind(..)
- ) where
-
-import qualified Data.Text as T
-import Database.SQLite.Simple (SQLData (SQLText))
-import Database.SQLite.Simple.FromField (FromField (fromField),
- fieldData)
-import Database.SQLite.Simple.Ok (Ok (Errors, Ok))
-import Database.SQLite.Simple.ToField (ToField (toField))
-
-data Kind =
- MonthlyPayment
- | WeeklyReport
- deriving (Eq, Show, Read)
-
-instance FromField Kind where
- fromField field = case fieldData field of
- SQLText text -> Ok (read (T.unpack text) :: Kind)
- _ -> Errors [error "SQLText field required for job kind"]
-
-instance ToField Kind where
- toField kind = SQLText . T.pack . show $ kind
diff --git a/server/src/Job/Model.hs b/server/src/Job/Model.hs
deleted file mode 100644
index 1dd6c63..0000000
--- a/server/src/Job/Model.hs
+++ /dev/null
@@ -1,49 +0,0 @@
-module Job.Model
- ( Job(..)
- , getLastExecution
- , actualizeLastExecution
- , actualizeLastCheck
- ) where
-
-import Data.Time.Clock (UTCTime, getCurrentTime)
-import Database.SQLite.Simple (Only (Only))
-import qualified Database.SQLite.Simple as SQLite
-import Prelude hiding (id)
-
-import Job.Kind
-import Model.Query (Query (Query))
-
-data Job = Job
- { id :: String
- , kind :: Kind
- , lastExecution :: Maybe UTCTime
- , lastCheck :: Maybe UTCTime
- } deriving (Show)
-
-getLastExecution :: Kind -> Query (Maybe UTCTime)
-getLastExecution jobKind =
- Query (\conn -> do
- result <- SQLite.query conn "SELECT last_execution FROM job WHERE kind = ?" (Only jobKind) :: IO [Only UTCTime]
- return $ case result of
- [Only time] -> Just time
- _ -> Nothing
- )
-
-actualizeLastExecution :: Kind -> UTCTime -> Query ()
-actualizeLastExecution jobKind time =
- Query (\conn -> do
- result <- SQLite.query conn "SELECT 1 FROM job WHERE kind = ?" (Only jobKind) :: IO [Only Int]
- let hasJob = case result of
- [Only _] -> True
- _ -> False
- if hasJob
- then SQLite.execute conn "UPDATE job SET last_execution = ? WHERE kind = ?" (time, jobKind)
- else SQLite.execute conn "INSERT INTO job (kind, last_execution, last_check) VALUES (?, ?, ?)" (jobKind, time, time)
- )
-
-actualizeLastCheck :: Kind -> Query ()
-actualizeLastCheck jobKind =
- Query (\conn -> do
- now <- getCurrentTime
- SQLite.execute conn "UPDATE job SET kind = ? WHERE last_check = ?" (jobKind, now)
- )
diff --git a/server/src/Job/MonthlyPayment.hs b/server/src/Job/MonthlyPayment.hs
deleted file mode 100644
index dfbe8b4..0000000
--- a/server/src/Job/MonthlyPayment.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Job.MonthlyPayment
- ( monthlyPayment
- ) where
-
-import Data.Time.Clock (UTCTime, getCurrentTime)
-
-import Common.Model (Frequency (..), Payment (..))
-import qualified Common.Util.Time as Time
-
-import qualified Model.Query as Query
-import qualified Persistence.Payment as PaymentPersistence
-
-monthlyPayment :: Maybe UTCTime -> IO UTCTime
-monthlyPayment _ = do
- monthlyPayments <- Query.run PaymentPersistence.listActiveMonthlyOrderedByName
- now <- getCurrentTime
- actualDay <- Time.timeToDay now
- let punctualPayments = map
- (\p -> p
- { _payment_frequency = Punctual
- , _payment_date = actualDay
- , _payment_createdAt = now
- })
- monthlyPayments
- _ <- Query.run (PaymentPersistence.createMany punctualPayments)
- return now
diff --git a/server/src/Job/WeeklyReport.hs b/server/src/Job/WeeklyReport.hs
deleted file mode 100644
index 282f2f1..0000000
--- a/server/src/Job/WeeklyReport.hs
+++ /dev/null
@@ -1,52 +0,0 @@
-module Job.WeeklyReport
- ( weeklyReport
- ) where
-
-import qualified Data.Map as M
-import qualified Data.Time.Clock as Clock
-import Data.Time.Clock (UTCTime, getCurrentTime)
-
-import Common.Model (User (..))
-
-import Conf (Conf)
-import qualified Model.Query as Query
-import qualified Persistence.Income as IncomePersistence
-import qualified Persistence.Payment as PaymentPersistence
-import qualified Persistence.User as UserPersistence
-import qualified SendMail
-import qualified View.Mail.WeeklyReport as WeeklyReport
-
-weeklyReport :: Conf -> Maybe UTCTime -> IO UTCTime
-weeklyReport conf mbLastExecution = do
- now <- getCurrentTime
-
- case mbLastExecution of
- Nothing ->
- return ()
-
- Just lastExecution -> do
- (weekPayments, cumulativeIncome, preIncomeRepartition, postIncomeRepartition, weekIncomes, users) <- Query.run $ do
- users <- UserPersistence.list
- paymentRange <- PaymentPersistence.getRange
- incomeDefinedForAll <- IncomePersistence.definedForAll (_user_id <$> users)
- cumulativeIncome <-
- case (incomeDefinedForAll, paymentRange) of
- (Just incomeStart, Just (paymentStart, _)) ->
- IncomePersistence.getCumulativeIncome (max incomeStart paymentStart) (Clock.utctDay now)
-
- _ ->
- return M.empty
- weekPayments <- PaymentPersistence.listModifiedPunctualSince lastExecution
- weekIncomes <- IncomePersistence.listModifiedSince lastExecution
- (preIncomeRepartition, postIncomeRepartition) <-
- PaymentPersistence.getPreAndPostPaymentRepartition paymentRange users
- return (weekPayments, cumulativeIncome, preIncomeRepartition, postIncomeRepartition, weekIncomes, users)
-
- _ <-
- SendMail.sendMail
- conf
- (WeeklyReport.mail conf users weekIncomes weekPayments cumulativeIncome preIncomeRepartition postIncomeRepartition lastExecution now)
-
- return ()
-
- return now
diff --git a/server/src/LoginSession.hs b/server/src/LoginSession.hs
deleted file mode 100644
index 86f1329..0000000
--- a/server/src/LoginSession.hs
+++ /dev/null
@@ -1,52 +0,0 @@
-module LoginSession
- ( put
- , get
- , delete
- ) where
-
-import Cookie (deleteCookie, getCookie,
- setSimpleCookie)
-import qualified Web.ClientSession as CS
-import Web.Scotty (ActionM)
-
-import Control.Monad.IO.Class (liftIO)
-
-import Data.Text (Text)
-import qualified Data.Text.Encoding as TE
-
-import Conf (Conf)
-
-sessionName :: Text
-sessionName = "SESSION"
-
-sessionKeyFile :: FilePath
-sessionKeyFile = "sessionKey"
-
-put :: Conf -> Text -> ActionM ()
-put conf value = do
- encrypted <- liftIO $ encrypt value
- setSimpleCookie conf sessionName encrypted
-
-encrypt :: Text -> IO Text
-encrypt value = do
- iv <- CS.randomIV
- key <- CS.getKey sessionKeyFile
- return . TE.decodeUtf8 $ CS.encrypt key iv (TE.encodeUtf8 value)
-
-get :: ActionM (Maybe Text)
-get = do
- maybeEncrypted <- getCookie sessionName
- case maybeEncrypted of
- Just encrypted ->
- liftIO $ decrypt encrypted
- Nothing ->
- return Nothing
-
-decrypt :: Text -> IO (Maybe Text)
-decrypt encrypted = do
- key <- CS.getKey sessionKeyFile
- let decrypted = TE.decodeUtf8 <$> CS.decrypt key (TE.encodeUtf8 encrypted)
- return decrypted
-
-delete :: Conf -> ActionM ()
-delete conf = deleteCookie conf sessionName
diff --git a/server/src/Main.hs b/server/src/Main.hs
deleted file mode 100644
index 659a0fa..0000000
--- a/server/src/Main.hs
+++ /dev/null
@@ -1,106 +0,0 @@
-module Main
- ( main
- ) where
-
-import qualified Network.HTTP.Types.Status as Status
-import Network.Wai.Middleware.Gzip (GzipFiles (GzipCompress))
-import qualified Network.Wai.Middleware.Gzip as W
-import Network.Wai.Middleware.Static
-import qualified Web.Scotty as S
-
-import qualified Conf
-import qualified Controller.Category as Category
-import qualified Controller.Income as Income
-import qualified Controller.Index as Index
-import qualified Controller.Payment as Payment
-import qualified Controller.Statistics as Statistics
-import qualified Controller.User as User
-import qualified Design.Global as Design
-import Job.Daemon (runDaemons)
-
-main :: IO ()
-main = do
- conf <- Conf.get "application.conf"
- putStrLn . show $ conf
- _ <- runDaemons conf
- S.scotty (Conf.port conf) $ do
-
- S.middleware $
- W.gzip $ W.def { W.gzipFiles = GzipCompress }
-
- S.middleware . staticPolicy $
- noDots >-> addBase "public"
-
- S.get "/css/main.css" $ do
- S.setHeader "Content-Type" "text/css"
- S.text Design.globalDesign
-
- S.post "/api/signIn" $
- S.jsonData >>= Index.signIn conf
-
- S.post "/api/signOut" $
- Index.signOut conf
-
- S.get "/api/users"$
- User.list
-
- S.get "/api/payments" $ do
- frequency <- S.param "frequency"
- page <- S.param "page"
- perPage <- S.param "perPage"
- search <- S.param "search"
- Payment.list (read frequency) page perPage search
-
- S.get "/api/payment/category" $ do
- name <- S.param "name"
- Payment.searchCategory name
-
- S.post "/api/payment" $
- S.jsonData >>= Payment.create
-
- S.put "/api/payment" $
- S.jsonData >>= Payment.edit
-
- S.delete "/api/payment/:id" $ do
- paymentId <- S.param "id"
- Payment.delete paymentId
-
- S.get "/api/incomes" $ do
- page <- S.param "page"
- perPage <- S.param "perPage"
- Income.list page perPage
-
- S.post "/api/income" $
- S.jsonData >>= Income.create
-
- S.put "/api/income" $
- S.jsonData >>= Income.edit
-
- S.delete "/api/income/:id" $ do
- incomeId <- S.param "id"
- Income.delete incomeId
-
- S.get "/api/allCategories" $ do
- Category.listAll
-
- S.get "/api/categories" $ do
- page <- S.param "page"
- perPage <- S.param "perPage"
- Category.list page perPage
-
- S.post "/api/category" $
- S.jsonData >>= Category.create
-
- S.put "/api/category" $
- S.jsonData >>= Category.edit
-
- S.delete "/api/category/:id" $ do
- categoryId <- S.param "id"
- Category.delete categoryId
-
- S.get "/api/statistics" $ do
- Statistics.paymentsAndIncomes
-
- S.notFound $ do
- S.status Status.ok200
- Index.get conf
diff --git a/server/src/Model/CreateCategory.hs b/server/src/Model/CreateCategory.hs
deleted file mode 100644
index dae061b..0000000
--- a/server/src/Model/CreateCategory.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-module Model.CreateCategory
- ( CreateCategory(..)
- ) where
-
-import Data.Text (Text)
-
-data CreateCategory = CreateCategory
- { _createCategory_name :: Text
- , _createCategory_color :: Text
- } deriving (Show)
diff --git a/server/src/Model/CreateIncome.hs b/server/src/Model/CreateIncome.hs
deleted file mode 100644
index 82451d2..0000000
--- a/server/src/Model/CreateIncome.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-module Model.CreateIncome
- ( CreateIncome(..)
- ) where
-
-import Data.Time.Calendar (Day)
-
-data CreateIncome = CreateIncome
- { _createIncome_amount :: Int
- , _createIncome_date :: Day
- } deriving (Show)
diff --git a/server/src/Model/CreatePayment.hs b/server/src/Model/CreatePayment.hs
deleted file mode 100644
index b25d2a4..0000000
--- a/server/src/Model/CreatePayment.hs
+++ /dev/null
@@ -1,16 +0,0 @@
-module Model.CreatePayment
- ( CreatePayment(..)
- ) where
-
-import Data.Text (Text)
-import Data.Time.Calendar (Day)
-
-import Common.Model (CategoryId, Frequency)
-
-data CreatePayment = CreatePayment
- { _createPayment_name :: Text
- , _createPayment_cost :: Int
- , _createPayment_date :: Day
- , _createPayment_category :: CategoryId
- , _createPayment_frequency :: Frequency
- } deriving (Show)
diff --git a/server/src/Model/EditCategory.hs b/server/src/Model/EditCategory.hs
deleted file mode 100644
index 8ee26ac..0000000
--- a/server/src/Model/EditCategory.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module Model.EditCategory
- ( EditCategory(..)
- ) where
-
-import Data.Text (Text)
-
-import Common.Model (CategoryId)
-
-data EditCategory = EditCategory
- { _editCategory_id :: CategoryId
- , _editCategory_name :: Text
- , _editCategory_color :: Text
- } deriving (Show)
diff --git a/server/src/Model/EditIncome.hs b/server/src/Model/EditIncome.hs
deleted file mode 100644
index ac3d311..0000000
--- a/server/src/Model/EditIncome.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module Model.EditIncome
- ( EditIncome(..)
- ) where
-
-import Data.Time.Calendar (Day)
-
-import Common.Model (IncomeId)
-
-data EditIncome = EditIncome
- { _editIncome_id :: IncomeId
- , _editIncome_amount :: Int
- , _editIncome_date :: Day
- } deriving (Show)
diff --git a/server/src/Model/EditPayment.hs b/server/src/Model/EditPayment.hs
deleted file mode 100644
index ac4c906..0000000
--- a/server/src/Model/EditPayment.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-module Model.EditPayment
- ( EditPayment(..)
- ) where
-
-import Data.Text (Text)
-import Data.Time.Calendar (Day)
-
-import Common.Model (CategoryId, Frequency, PaymentId)
-
-data EditPayment = EditPayment
- { _editPayment_id :: PaymentId
- , _editPayment_name :: Text
- , _editPayment_cost :: Int
- , _editPayment_date :: Day
- , _editPayment_category :: CategoryId
- , _editPayment_frequency :: Frequency
- } deriving (Show)
diff --git a/server/src/Model/HashedPassword.hs b/server/src/Model/HashedPassword.hs
deleted file mode 100644
index c71e372..0000000
--- a/server/src/Model/HashedPassword.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Model.HashedPassword
- ( hash
- , check
- , HashedPassword(..)
- ) where
-
-import qualified Crypto.BCrypt as BCrypt
-import Data.Text (Text)
-import qualified Data.Text.Encoding as TE
-
-import Common.Model.Password (Password (..))
-
-newtype HashedPassword = HashedPassword Text deriving (Show)
-
-hash :: Password -> IO (Maybe HashedPassword)
-hash (Password p) = do
- hashed <- BCrypt.hashPasswordUsingPolicy BCrypt.slowerBcryptHashingPolicy (TE.encodeUtf8 p)
- case hashed of
- Nothing ->
- return Nothing
-
- Just h ->
- return . Just . HashedPassword . TE.decodeUtf8 $ h
-
-check :: Password -> HashedPassword -> Bool
-check (Password p) (HashedPassword h) =
- BCrypt.validatePassword (TE.encodeUtf8 h) (TE.encodeUtf8 p)
diff --git a/server/src/Model/IncomeResource.hs b/server/src/Model/IncomeResource.hs
deleted file mode 100644
index 6ab5f18..0000000
--- a/server/src/Model/IncomeResource.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module Model.IncomeResource
- ( IncomeResource(..)
- ) where
-
-import Common.Model (Income (..))
-
-import Resource (Resource, resourceCreatedAt, resourceDeletedAt,
- resourceEditedAt)
-
-newtype IncomeResource = IncomeResource Income
-
-instance Resource IncomeResource where
- resourceCreatedAt (IncomeResource i) = _income_createdAt i
- resourceEditedAt (IncomeResource i) = _income_editedAt i
- resourceDeletedAt (IncomeResource i) = _income_deletedAt i
diff --git a/server/src/Model/Mail.hs b/server/src/Model/Mail.hs
deleted file mode 100644
index 780efcc..0000000
--- a/server/src/Model/Mail.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-module Model.Mail
- ( Mail(..)
- ) where
-
-import Data.Text (Text)
-
-data Mail = Mail
- { from :: Text
- , to :: [Text]
- , subject :: Text
- , body :: Text
- } deriving (Eq, Show)
diff --git a/server/src/Model/PaymentResource.hs b/server/src/Model/PaymentResource.hs
deleted file mode 100644
index 1ea978c..0000000
--- a/server/src/Model/PaymentResource.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module Model.PaymentResource
- ( PaymentResource(..)
- ) where
-
-import Common.Model (Payment (..))
-
-import Resource (Resource, resourceCreatedAt, resourceDeletedAt,
- resourceEditedAt)
-
-newtype PaymentResource = PaymentResource Payment
-
-instance Resource PaymentResource where
- resourceCreatedAt (PaymentResource p) = _payment_createdAt p
- resourceEditedAt (PaymentResource p) = _payment_editedAt p
- resourceDeletedAt (PaymentResource p) = _payment_deletedAt p
diff --git a/server/src/Model/Query.hs b/server/src/Model/Query.hs
deleted file mode 100644
index 22ae95b..0000000
--- a/server/src/Model/Query.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-module Model.Query
- ( Query(..)
- , run
- ) where
-
-import Data.Functor (Functor)
-import Database.SQLite.Simple (Connection)
-import qualified Database.SQLite.Simple as SQLite
-
-data Query a = Query (Connection -> IO a)
-
-instance Functor Query where
- fmap f (Query call) = Query (fmap f . call)
-
-instance Applicative Query where
- pure x = Query (const $ return x)
- (Query callF) <*> (Query callX) = Query (\conn -> do
- x <- callX conn
- f <- callF conn
- return (f x))
-
-instance Monad Query where
- (Query callX) >>= f = Query (\conn -> do
- x <- callX conn
- case f x of Query callY -> callY conn)
-
-run :: Query a -> IO a
-run (Query call) = do
- conn <- SQLite.open "database"
- result <- call conn
- _ <- SQLite.close conn
- return result
diff --git a/server/src/Model/SignIn.hs b/server/src/Model/SignIn.hs
deleted file mode 100644
index a217bae..0000000
--- a/server/src/Model/SignIn.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-module Model.SignIn
- ( SignIn(..)
- ) where
-
-import Common.Model (Email, Password)
-
-data SignIn = SignIn
- { _signIn_email :: Email
- , _signIn_password :: Password
- } deriving Show
diff --git a/server/src/Model/UUID.hs b/server/src/Model/UUID.hs
deleted file mode 100644
index 0959a8e..0000000
--- a/server/src/Model/UUID.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-module Model.UUID
- ( generateUUID
- ) where
-
-import Data.Text (Text, pack)
-import Data.UUID (toString)
-import Data.UUID.V4 (nextRandom)
-
-generateUUID :: IO Text
-generateUUID = pack . toString <$> nextRandom
diff --git a/server/src/Payer.hs b/server/src/Payer.hs
deleted file mode 100644
index ab8312e..0000000
--- a/server/src/Payer.hs
+++ /dev/null
@@ -1,87 +0,0 @@
-module Payer
- ( getExceedingPayers
- ) where
-
-import Data.Map (Map)
-import qualified Data.Map as M
-
-import Common.Model (ExceedingPayer (..), User (..), UserId)
-
-data Payer = Payer
- { _payer_userId :: UserId
- , _payer_preIncomePayments :: Int
- , _payer_postIncomePayments :: Int
- , _payer_income :: Int
- }
-
-data PostPaymentPayer = PostPaymentPayer
- { _postPaymentPayer_userId :: UserId
- , _postPaymentPayer_preIncomePayments :: Int
- , _postPaymentPayer_cumulativeIncome :: Int
- , _postPaymentPayer_ratio :: Float
- }
-
-getExceedingPayers :: [User] -> Map UserId Int -> Map UserId Int -> Map UserId Int -> [ExceedingPayer]
-getExceedingPayers users cumulativeIncome preIncomeRepartition postIncomeRepartition =
- let userIds = map _user_id users
- payers = getPayers userIds cumulativeIncome preIncomeRepartition postIncomeRepartition
- postPaymentPayers = map getPostPaymentPayer payers
- mbMaxRatio = safeMaximum . map _postPaymentPayer_ratio $ postPaymentPayers
- in case mbMaxRatio of
- Just maxRatio ->
- exceedingPayersFromAmounts
- . map (\p -> (_postPaymentPayer_userId p, getFinalDiff maxRatio p))
- $ postPaymentPayers
- Nothing ->
- exceedingPayersFromAmounts
- . map (\p -> (_payer_userId p, _payer_preIncomePayments p))
- $ payers
-
-getPayers :: [UserId] -> Map UserId Int -> Map UserId Int -> Map UserId Int -> [Payer]
-getPayers userIds cumulativeIncome preIncomeRepartition postIncomeRepartition =
- flip map userIds (\userId -> Payer
- { _payer_userId = userId
- , _payer_preIncomePayments = M.findWithDefault 0 userId preIncomeRepartition
- , _payer_postIncomePayments = M.findWithDefault 0 userId postIncomeRepartition
- , _payer_income = M.findWithDefault 0 userId cumulativeIncome
- }
- )
-
-exceedingPayersFromAmounts :: [(UserId, Int)] -> [ExceedingPayer]
-exceedingPayersFromAmounts userAmounts =
- case mbMinAmount of
- Nothing ->
- []
- Just minAmount ->
- filter (\payer -> _exceedingPayer_amount payer > 0)
- . map (\userAmount ->
- ExceedingPayer
- { _exceedingPayer_userId = fst userAmount
- , _exceedingPayer_amount = snd userAmount - minAmount
- }
- )
- $ userAmounts
- where mbMinAmount = safeMinimum . map snd $ userAmounts
-
-getPostPaymentPayer :: Payer -> PostPaymentPayer
-getPostPaymentPayer payer =
- PostPaymentPayer
- { _postPaymentPayer_userId = _payer_userId payer
- , _postPaymentPayer_preIncomePayments = _payer_preIncomePayments payer
- , _postPaymentPayer_cumulativeIncome = _payer_income payer
- , _postPaymentPayer_ratio = (fromIntegral . _payer_postIncomePayments $ payer) / (fromIntegral $ _payer_income payer)
- }
-
-getFinalDiff :: Float -> PostPaymentPayer -> Int
-getFinalDiff maxRatio payer =
- let postIncomeDiff =
- truncate $ -1.0 * (maxRatio - _postPaymentPayer_ratio payer) * (fromIntegral . _postPaymentPayer_cumulativeIncome $ payer)
- in postIncomeDiff + _postPaymentPayer_preIncomePayments payer
-
-safeMinimum :: (Ord a) => [a] -> Maybe a
-safeMinimum [] = Nothing
-safeMinimum xs = Just . minimum $ xs
-
-safeMaximum :: (Ord a) => [a] -> Maybe a
-safeMaximum [] = Nothing
-safeMaximum xs = Just . maximum $ xs
diff --git a/server/src/Persistence/Category.hs b/server/src/Persistence/Category.hs
deleted file mode 100644
index b0a6fca..0000000
--- a/server/src/Persistence/Category.hs
+++ /dev/null
@@ -1,123 +0,0 @@
-module Persistence.Category
- ( count
- , list
- , listAll
- , create
- , edit
- , delete
- ) where
-
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import Data.Time.Clock (getCurrentTime)
-import Database.SQLite.Simple (FromRow (fromRow), NamedParam ((:=)))
-import qualified Database.SQLite.Simple as SQLite
-import Prelude hiding (id)
-
-import Common.Model (Category (..), CategoryId)
-
-import Model.Query (Query (Query))
-
-newtype Row = Row Category
-
-instance FromRow Row where
- fromRow = Row <$> (Category <$>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field)
-
-data CountRow = CountRow Int
-
-instance FromRow CountRow where
- fromRow = CountRow <$> SQLite.field
-
-count :: Query Int
-count =
- Query (\conn ->
- (Maybe.fromMaybe 0 . fmap (\(CountRow n) -> n) . Maybe.listToMaybe) <$>
- SQLite.query_ conn "SELECT COUNT(*) FROM category WHERE deleted_at IS NULL"
- )
-
-
-list :: Int -> Int -> Query [Category]
-list page perPage =
- Query (\conn ->
- map (\(Row c) -> c) <$>
- SQLite.queryNamed
- conn
- "SELECT * FROM category WHERE deleted_at IS NULL ORDER BY name LIMIT :limit OFFSET :offset"
- [ ":limit" := perPage
- , ":offset" := (page - 1) * perPage
- ]
- )
-
-listAll :: Query [Category]
-listAll =
- Query (\conn ->
- map (\(Row c) -> c) <$>
- SQLite.query_ conn "SELECT * FROM category WHERE deleted_at IS NULL"
- )
-
-create :: Text -> Text -> Query ()
-create name color =
- Query (\conn -> do
- currentTime <- getCurrentTime
- SQLite.executeNamed
- conn
- "INSERT INTO category (name, color, created_at) VALUES (:name, :color, :created_at)"
- [ ":name" := name
- , ":color" := color
- , ":created_at" := currentTime
- ]
- )
-
-edit :: CategoryId -> Text -> Text -> Query Bool
-edit id name color =
- Query (\conn -> do
- mbCategory <- fmap (\(Row c) -> c) . Maybe.listToMaybe <$>
- (SQLite.queryNamed conn "SELECT * FROM category WHERE id = :id" [ ":id" := id ])
- if Maybe.isJust mbCategory
- then do
- currentTime <- getCurrentTime
- SQLite.executeNamed
- conn
- "UPDATE category SET edited_at = :editedAt, name = :name, color = :color WHERE id = :id"
- [ ":editedAt" := currentTime
- , ":name" := name
- , ":color" := color
- , ":id" := id
- ]
- return True
- else
- return False
- )
-
-data BoolRow = BoolRow Int
-
-instance FromRow BoolRow where
- fromRow = BoolRow <$> SQLite.field
-
-delete :: CategoryId -> Query Bool
-delete id =
- Query (\conn -> do
- mbPayment <- (fmap (\(BoolRow b) -> b) . Maybe.listToMaybe) <$>
- (SQLite.queryNamed
- conn
- "SELECT true FROM payment WHERE category = :id AND deleted_at IS NULL"
- [ ":id" := id ])
- if Maybe.isNothing mbPayment
- then do
- currentTime <- getCurrentTime
- SQLite.executeNamed
- conn
- "UPDATE category SET deleted_at = :deletedAt WHERE id = :id AND deleted_at IS NULL"
- [ ":deletedAt" := currentTime
- , ":id" := id
- ]
- return True
- else
- return False
- )
diff --git a/server/src/Persistence/Frequency.hs b/server/src/Persistence/Frequency.hs
deleted file mode 100644
index edaa844..0000000
--- a/server/src/Persistence/Frequency.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-module Persistence.Frequency
- ( FrequencyField(..)
- ) where
-
-import qualified Data.Text as T
-import Database.SQLite.Simple (SQLData (SQLText))
-import Database.SQLite.Simple.FromField (FromField (fromField),
- fieldData)
-import Database.SQLite.Simple.Ok (Ok (Errors, Ok))
-import Database.SQLite.Simple.ToField (ToField (toField))
-
-import Common.Model (Frequency)
-
-newtype FrequencyField = FrequencyField Frequency
-
-instance FromField FrequencyField where
- fromField field =
- case fieldData field of
- SQLText text -> Ok (FrequencyField (read (T.unpack text) :: Frequency))
- _ -> Errors [error "SQLText field required for frequency"]
-
-instance ToField FrequencyField where
- toField (FrequencyField f) = SQLText . T.pack . show $ f
diff --git a/server/src/Persistence/Income.hs b/server/src/Persistence/Income.hs
deleted file mode 100644
index 1b5364c..0000000
--- a/server/src/Persistence/Income.hs
+++ /dev/null
@@ -1,201 +0,0 @@
-module Persistence.Income
- ( listAll
- , count
- , list
- , listModifiedSince
- , create
- , edit
- , delete
- , definedForAll
- , getCumulativeIncome
- ) where
-
-import qualified Data.List as L
-import Data.Map (Map)
-import qualified Data.Map as M
-import qualified Data.Maybe as Maybe
-import qualified Data.Text as T
-import Data.Time.Calendar (Day)
-import Data.Time.Clock (UTCTime)
-import Data.Time.Clock (getCurrentTime)
-import Database.SQLite.Simple (FromRow (fromRow), NamedParam ((:=)))
-import qualified Database.SQLite.Simple as SQLite
-import Prelude hiding (id, until)
-
-import Common.Model (Income (..), IncomeId, PaymentId,
- UserId)
-
-import Model.Query (Query (Query))
-
-newtype Row = Row Income
-
-instance FromRow Row where
- fromRow = Row <$> (Income <$>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field)
-
-data CountRow = CountRow Int
-
-instance FromRow CountRow where
- fromRow = CountRow <$> SQLite.field
-
-listAll :: Query [Income]
-listAll =
- Query (\conn ->
- map (\(Row i) -> i) <$>
- SQLite.query_
- conn
- "SELECT * FROM income WHERE deleted_at IS NULL ORDER BY date DESC"
- )
-
-
-count :: Query Int
-count =
- Query (\conn ->
- (Maybe.fromMaybe 0 . fmap (\(CountRow n) -> n) . Maybe.listToMaybe) <$>
- SQLite.query_ conn "SELECT COUNT(*) FROM income WHERE deleted_at IS NULL"
- )
-
-list :: Int -> Int -> Query [Income]
-list page perPage =
- Query (\conn ->
- map (\(Row i) -> i) <$>
- SQLite.queryNamed
- conn
- "SELECT * FROM income WHERE deleted_at IS NULL ORDER BY date DESC LIMIT :limit OFFSET :offset"
- [ ":limit" := perPage
- , ":offset" := (page - 1) * perPage
- ]
- )
-
-listModifiedSince :: UTCTime -> Query [Income]
-listModifiedSince since =
- Query (\conn ->
- map (\(Row i) -> i) <$>
- SQLite.queryNamed
- conn
- (SQLite.Query . T.intercalate " " $
- [ "SELECT *"
- , "FROM income"
- , "WHERE"
- , "created_at >= :since"
- , "OR edited_at >= :since"
- , "OR deleted_at >= :since"
- ])
- [ ":since" := since ]
- )
-
-create :: UserId -> Day -> Int -> Query ()
-create userId date amount =
- Query (\conn -> do
- createdAt <- getCurrentTime
- SQLite.executeNamed
- conn
- "INSERT INTO income (user_id, date, amount, created_at) VALUES (:userId, :date, :amount, :createdAt)"
- [ ":userId" := userId
- , ":date" := date
- , ":amount" := amount
- , ":createdAt" := createdAt
- ]
- )
-
-edit :: UserId -> IncomeId -> Day -> Int -> Query Bool
-edit userId id date amount =
- Query (\conn -> do
- income <- fmap (\(Row i) -> i) . Maybe.listToMaybe <$>
- SQLite.queryNamed conn "SELECT * FROM income WHERE id = :id" [ ":id" := id ]
- if Maybe.isJust income then
- do
- currentTime <- getCurrentTime
- SQLite.executeNamed
- conn
- "UPDATE income SET edited_at = :editedAt, date = :date, amount = :amount WHERE id = :id AND user_id = :userId"
- [ ":editedAt" := currentTime
- , ":date" := date
- , ":amount" := amount
- , ":id" := id
- , ":userId" := userId
- ]
- return True
- else
- return False
- )
-
-delete :: UserId -> PaymentId -> Query ()
-delete userId id =
- Query (\conn ->
- SQLite.executeNamed
- conn
- "UPDATE income SET deleted_at = datetime('now') WHERE id = :id AND user_id = :userId"
- [ ":id" := id
- , ":userId" := userId
- ]
- )
-
-data UserDayRow = UserDayRow (UserId, Day)
-
-instance FromRow UserDayRow where
- fromRow = do
- user <- SQLite.field
- day <- SQLite.field
- return $ UserDayRow (user, day)
-
-definedForAll :: [UserId] -> Query (Maybe Day)
-definedForAll users =
- Query (\conn ->
- (fromRows . fmap (\(UserDayRow (user, day)) -> (user, day))) <$>
- SQLite.query_
- conn
- "SELECT user_id, MIN(date) FROM income WHERE deleted_at IS NULL GROUP BY user_id;"
- )
- where
- fromRows rows =
- if L.sort users == L.sort (map fst rows) then
- Maybe.listToMaybe . reverse . L.sort . map snd $ rows
- else
- Nothing
-
-getCumulativeIncome :: Day -> Day -> Query (Map UserId Int)
-getCumulativeIncome start end =
- Query (\conn -> M.fromList <$> SQLite.queryNamed conn (SQLite.Query query) parameters)
- where
- query =
- T.intercalate "\n" $
- [ "SELECT user_id, CAST(ROUND(SUM(count)) AS INTEGER) FROM ("
- , " SELECT"
- , " I1.user_id,"
- , " ((JULIANDAY(MIN(I2.date)) - JULIANDAY(I1.date)) * I1.amount * 12 / 365) AS count"
- , " FROM (" <> (selectBoundedIncomes ">" ":start") <> ") AS I1"
- , " INNER JOIN (" <> (selectBoundedIncomes "<" ":end") <> ") AS I2"
- , " ON I2.date > I1.date AND I2.user_id == I1.user_id"
- , " GROUP BY I1.date, I1.user_id"
- , ") GROUP BY user_id"
- ]
-
- selectBoundedIncomes op param =
- T.intercalate "\n" $
- [ " SELECT user_id, date, amount FROM ("
- , " SELECT"
- , " i.user_id, " <> param <> " AS date, i.amount"
- , " FROM"
- , " (SELECT id, MAX(date) AS max_date"
- , " FROM income"
- , " WHERE date <= " <> param <> " AND deleted_at IS NULL"
- , " GROUP BY user_id) AS m"
- , " INNER JOIN income AS i"
- , " ON i.id = m.id AND i.date = m.max_date"
- , " ) UNION"
- , " SELECT user_id, date, amount"
- , " FROM income"
- , " WHERE date " <> op <> " " <> param <> " AND deleted_at IS NULL"
- ]
-
- parameters =
- [ ":start" := start
- , ":end" := end
- ]
diff --git a/server/src/Persistence/Payment.hs b/server/src/Persistence/Payment.hs
deleted file mode 100644
index 573d57f..0000000
--- a/server/src/Persistence/Payment.hs
+++ /dev/null
@@ -1,389 +0,0 @@
-module Persistence.Payment
- ( count
- , find
- , getRange
- , listAllPunctual
- , listActivePage
- , listModifiedPunctualSince
- , listActiveMonthlyOrderedByName
- , create
- , createMany
- , edit
- , delete
- , searchCategory
- , repartition
- , getPreAndPostPaymentRepartition
- , usedCategories
- ) where
-
-import Data.Map (Map)
-import qualified Data.Map as M
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time.Calendar (Day)
-import qualified Data.Time.Calendar as Calendar
-import Data.Time.Clock (UTCTime)
-import Data.Time.Clock (getCurrentTime)
-import Database.SQLite.Simple (FromRow (fromRow),
- NamedParam ((:=)), ToRow)
-import qualified Database.SQLite.Simple as SQLite
-import Database.SQLite.Simple.ToField (ToField (toField))
-import Prelude hiding (id, until)
-
-import Common.Model (CategoryId, Frequency (..),
- Payment (..), PaymentId,
- User (..), UserId)
-import qualified Common.Util.Text as TextUtil
-
-import Model.Query (Query (Query))
-import Persistence.Frequency (FrequencyField (..))
-import qualified Persistence.Income as IncomePersistence
-import qualified Persistence.Util as PersistenceUtil
-
-
-fields :: Text
-fields = T.intercalate "," $
- [ "id"
- , "user_id"
- , "name"
- , "cost"
- , "date"
- , "category"
- , "frequency"
- , "created_at"
- , "edited_at"
- , "deleted_at"
- ]
-
-newtype Row = Row Payment
-
-instance FromRow Row where
- fromRow = Row <$> (Payment <$>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- (fmap (\(FrequencyField f) -> f) $ SQLite.field) <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field)
-
-newtype InsertRow = InsertRow Payment
-
-instance ToRow InsertRow where
- toRow (InsertRow p) =
- [ toField (_payment_user p)
- , toField (_payment_name p)
- , toField (_payment_cost p)
- , toField (_payment_date p)
- , toField (_payment_category p)
- , toField (FrequencyField (_payment_frequency p))
- , toField (_payment_createdAt p)
- ]
-
-data Count = Count Int
-
-instance FromRow Count where
- fromRow = Count <$> SQLite.field
-
-count :: Frequency -> Text -> Query Int
-count frequency search =
- Query (\conn ->
- (\[Count n] -> n) <$>
- SQLite.queryNamed
- conn
- (SQLite.Query $ T.intercalate " "
- [ "SELECT COUNT(*)"
- , "FROM payment"
- , "WHERE"
- , "deleted_at IS NULL"
- , "AND frequency = :frequency"
- , "AND (" <> PersistenceUtil.formatKeyForSearch "name" <> " LIKE :search OR cost LIKE :search)"
- ])
- [ ":frequency" := FrequencyField frequency
- , ":search" := "%" <> TextUtil.formatSearch search <> "%"
- ]
- )
-
-find :: PaymentId -> Query (Maybe Payment)
-find paymentId =
- Query (\conn -> do
- fmap (\(Row p) -> p) . Maybe.listToMaybe <$>
- SQLite.queryNamed
- conn
- (SQLite.Query $ "SELECT " <> fields <> " FROM payment WHERE id = :id")
- [ "id" := paymentId
- ]
- )
-
-data RangeRow = RangeRow (Day, Day)
-
-instance FromRow RangeRow where
- fromRow = (\f t -> RangeRow (f, t)) <$> SQLite.field <*> SQLite.field
-
-getRange :: Query (Maybe (Day, Day))
-getRange =
- Query (\conn -> do
- fmap (\(RangeRow (f, t)) -> (f, t)) . Maybe.listToMaybe <$>
- SQLite.queryNamed
- conn
- (SQLite.Query $ T.intercalate " "
- [ "SELECT MIN(date), MAX(date)"
- , "FROM payment"
- , "WHERE"
- , "frequency = :frequency"
- , "AND deleted_at IS NULL"
- ])
- [ ":frequency" := FrequencyField Punctual
- ]
- )
-
-listAllPunctual :: Query [Payment]
-listAllPunctual =
- Query (\conn ->
- map (\(Row p) -> p) <$>
- SQLite.queryNamed
- conn
- (SQLite.Query $ T.intercalate " "
- [ "SELECT"
- , fields
- , "FROM payment"
- , "WHERE deleted_at IS NULL AND frequency = :frequency"
- , "ORDER BY date"
- ])
- [ ":frequency" := FrequencyField Punctual
- ]
- )
-
-
-listActivePage :: Frequency -> Int -> Int -> Text -> Query [Payment]
-listActivePage frequency page perPage search =
- Query (\conn ->
- map (\(Row p) -> p) <$>
- SQLite.queryNamed
- conn
- (SQLite.Query $ T.intercalate " "
- [ "SELECT"
- , fields
- , "FROM payment"
- , "WHERE"
- , "deleted_at IS NULL"
- , "AND frequency = :frequency"
- , "AND (" <> PersistenceUtil.formatKeyForSearch "name" <> " LIKE :search OR cost LIKE :search)"
- , "ORDER BY date DESC"
- , "LIMIT :limit"
- , "OFFSET :offset"
- ]
- )
- [ ":frequency" := FrequencyField frequency
- , ":search" := "%" <> TextUtil.formatSearch search <> "%"
- , ":limit" := perPage
- , ":offset" := (page - 1) * perPage
- ]
- )
-
-listModifiedPunctualSince :: UTCTime -> Query [Payment]
-listModifiedPunctualSince since =
- Query (\conn ->
- map (\(Row i) -> i) <$>
- SQLite.queryNamed
- conn
- (SQLite.Query . T.intercalate " " $
- [ "SELECT " <> fields
- , "FROM payment"
- , "WHERE"
- , "frequency = :frequency"
- , "AND (created_at >= :since OR edited_at >= :since OR deleted_at >= :since)"
- ])
- [ ":frequency" := FrequencyField Punctual
- , ":since" := since
- ]
- )
-
-
-listActiveMonthlyOrderedByName :: Query [Payment]
-listActiveMonthlyOrderedByName =
- Query (\conn -> do
- map (\(Row p) -> p) <$>
- SQLite.queryNamed
- conn
- (SQLite.Query $ T.intercalate " "
- [ "SELECT"
- , fields
- , "FROM payment"
- , "WHERE deleted_at IS NULL AND frequency = :frequency"
- , "ORDER BY name DESC"
- ])
- [ ":frequency" := FrequencyField Monthly
- ]
- )
-
-create :: UserId -> Text -> Int -> Day -> CategoryId -> Frequency -> Query ()
-create userId name cost date category frequency =
- Query (\conn -> do
- currentTime <- getCurrentTime
- SQLite.executeNamed
- conn
- (SQLite.Query $ T.intercalate " "
- [ "INSERT INTO payment (user_id, name, cost, date, category, frequency, created_at)"
- , "VALUES (:userId, :name, :cost, :date, :category, :frequency, :currentTime)"
- ])
- [ ":userId" := userId
- , ":name" := name
- , ":cost" := cost
- , ":date" := date
- , ":category" := category
- , ":frequency" := FrequencyField frequency
- , ":currentTime" := currentTime
- ]
- )
-
-createMany :: [Payment] -> Query ()
-createMany payments =
- Query (\conn ->
- SQLite.executeMany
- conn
- (SQLite.Query $ T.intercalate ""
- [ "INSERT INTO payment (user_id, name, cost, date, category, frequency, created_at)"
- , "VALUES (?, ?, ?, ?, ?, ?, ?)"
- ])
- (map InsertRow payments)
- )
-
-edit :: UserId -> PaymentId -> Text -> Int -> Day -> CategoryId -> Frequency -> Query Bool
-edit userId paymentId name cost date category frequency =
- Query (\conn -> do
- payment <- fmap (\(Row p) -> p) . Maybe.listToMaybe <$>
- SQLite.queryNamed
- conn
- (SQLite.Query $
- "SELECT " <> fields <> " FROM payment WHERE id = :paymentId and user_id = :userId")
- [ ":paymentId" := paymentId
- , ":userId" := userId
- ]
- if Maybe.isJust payment then
- do
- currentTime <- getCurrentTime
- SQLite.executeNamed
- conn
- (SQLite.Query $ T.intercalate " "
- [ "UPDATE"
- , " payment"
- , "SET"
- , " edited_at = :editedAt,"
- , " name = :name,"
- , " cost = :cost,"
- , " date = :date,"
- , " category = :category,"
- , " frequency = :frequency"
- , "WHERE"
- , " id = :id"
- , " AND user_id = :userId"
- ])
- [ ":editedAt" := currentTime
- , ":name" := name
- , ":cost" := cost
- , ":date" := date
- , ":category" := category
- , ":frequency" := FrequencyField frequency
- , ":id" := paymentId
- , ":userId" := userId
- ]
- return True
- else
- return False
- )
-
-delete :: UserId -> PaymentId -> Query ()
-delete userId paymentId =
- Query (\conn ->
- SQLite.executeNamed
- conn
- "UPDATE payment SET deleted_at = datetime('now') WHERE id = :id AND user_id = :userId"
- [ ":id" := paymentId
- , ":userId" := userId
- ]
- )
-
-data CategoryIdRow = CategoryIdRow CategoryId
-
-instance FromRow CategoryIdRow where
- fromRow = CategoryIdRow <$> SQLite.field
-
-searchCategory :: Text -> Query (Maybe CategoryId)
-searchCategory paymentName =
- Query (\conn ->
- fmap (\(CategoryIdRow d) -> d) . Maybe.listToMaybe <$>
- SQLite.queryNamed
- conn
- (SQLite.Query . T.intercalate " " $
- [ "SELECT category"
- , "FROM payment"
- , "WHERE deleted_at is NULL AND name LIKE :name"
- , "ORDER BY edited_at, created_at"
- , "LIMIT 1"
- ])
- [ ":name" := "%" <> paymentName <> "%"
- ]
- )
-
-usedCategories :: Query [CategoryId]
-usedCategories =
- Query (\conn -> do
- map (\(CategoryIdRow p) -> p) <$>
- SQLite.query_
- conn
- (SQLite.Query $ T.intercalate " "
- [ "SELECT DISTINCT category"
- , "FROM payment"
- , "WHERE deleted_at IS NULL"
- ])
- )
-
-data UserCostRow = UserCostRow (UserId, Int)
-
-instance FromRow UserCostRow where
- fromRow = do
- user <- SQLite.field
- cost <- SQLite.field
- return $ UserCostRow (user, cost)
-
-repartition :: Frequency -> Text -> Day -> Day -> Query (Map UserId Int)
-repartition frequency search from to =
- Query (\conn ->
- M.fromList . fmap (\(UserCostRow r) -> r) <$> SQLite.queryNamed
- conn
- (SQLite.Query . T.intercalate " " $
- [ "SELECT user_id, SUM(cost)"
- , "FROM payment"
- , "WHERE"
- , "deleted_at IS NULL"
- , "AND frequency = :frequency"
- , "AND (" <> PersistenceUtil.formatKeyForSearch "name" <> " LIKE :search OR cost LIKE :search)"
- , "AND date >= :from"
- , "AND date < :to"
- , "GROUP BY user_id"
- ])
- [ ":frequency" := FrequencyField frequency
- , ":search" := "%" <> TextUtil.formatSearch search <> "%"
- , ":from" := from
- , ":to" := to
- ]
- )
-
-getPreAndPostPaymentRepartition :: Maybe (Day, Day) -> [User] -> Query (Map UserId Int, Map UserId Int)
-getPreAndPostPaymentRepartition paymentRange users = do
- case paymentRange of
- Just (from, to) -> do
- incomeDefinedForAll <- IncomePersistence.definedForAll (_user_id <$> users)
- (,)
- <$> (repartition Punctual "" from (Maybe.fromMaybe (Calendar.addDays 1 to) incomeDefinedForAll))
- <*> (case incomeDefinedForAll of
- Just d -> repartition Punctual "" d (Calendar.addDays 1 to)
- Nothing -> return M.empty)
-
- Nothing ->
- return (M.empty, M.empty)
diff --git a/server/src/Persistence/User.hs b/server/src/Persistence/User.hs
deleted file mode 100644
index 12145ac..0000000
--- a/server/src/Persistence/User.hs
+++ /dev/null
@@ -1,78 +0,0 @@
-module Persistence.User
- ( list
- , get
- , checkPassword
- , createSignInToken
- ) where
-
-import qualified Data.Maybe as Maybe
-import Data.Text (Text)
-import Database.SQLite.Simple (FromRow (fromRow), NamedParam ((:=)))
-import qualified Database.SQLite.Simple as SQLite
-
-import Common.Model (Email (..), Password (..), User (..))
-
-import Model.HashedPassword (HashedPassword (..))
-import qualified Model.HashedPassword as HashedPassword
-import Model.Query (Query (Query))
-import qualified Model.UUID as UUID
-
-newtype Row = Row User
-
-instance FromRow Row where
- fromRow = Row <$> (User <$>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field <*>
- SQLite.field)
-
-list :: Query [User]
-list =
- Query (\conn -> do
- map (\(Row u) -> u) <$>
- SQLite.query_ conn "SELECT id, creation, email, name from user ORDER BY creation DESC"
- )
-
-get :: Text -> Query (Maybe User)
-get token =
- Query (\conn -> do
- fmap (\(Row u) -> u) . Maybe.listToMaybe <$>
- SQLite.queryNamed
- conn
- "SELECT id, creation, email, name FROM user WHERE sign_in_token = :sign_in_token LIMIT 1"
- [ ":sign_in_token" := token ]
- )
-
-data HashedPasswordRow = HashedPasswordRow HashedPassword
-
-instance FromRow HashedPasswordRow where
- fromRow = HashedPasswordRow <$> (HashedPassword <$> SQLite.field)
-
-checkPassword :: Email -> Password -> Query Bool
-checkPassword (Email email) password =
- Query (\conn -> do
- hashedPassword <- fmap (\(HashedPasswordRow p) -> p) . Maybe.listToMaybe <$>
- SQLite.queryNamed
- conn
- "SELECT password FROM user WHERE email = :email LIMIT 1"
- [ ":email" := email ]
- case hashedPassword of
- Just h ->
- return (HashedPassword.check password h)
-
- Nothing ->
- return False
- )
-
-createSignInToken :: Email -> Query Text
-createSignInToken (Email email) =
- Query (\conn -> do
- token <- UUID.generateUUID
- SQLite.executeNamed
- conn
- "UPDATE user SET sign_in_token = :sign_in_token WHERE email = :email"
- [ ":sign_in_token" := token
- , ":email" := email
- ]
- return token
- )
diff --git a/server/src/Persistence/Util.hs b/server/src/Persistence/Util.hs
deleted file mode 100644
index b7496c6..0000000
--- a/server/src/Persistence/Util.hs
+++ /dev/null
@@ -1,11 +0,0 @@
-module Persistence.Util
- ( formatKeyForSearch
- ) where
-
-import Data.Text (Text)
-
-formatKeyForSearch :: Text -> Text
-formatKeyForSearch key =
- "replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(lower("
- <> key
- <> "), 'à', 'a'), 'â', 'a'), 'ç', 'c'), 'è', 'e'), 'é', 'e'), 'ê', 'e'), 'ë', 'e'), 'î', 'i'), 'ï', 'i'), 'ô', 'o'), 'ù', 'u'), 'û', 'u'), 'ü', 'u')"
diff --git a/server/src/Resource.hs b/server/src/Resource.hs
deleted file mode 100644
index a12a0f2..0000000
--- a/server/src/Resource.hs
+++ /dev/null
@@ -1,54 +0,0 @@
-module Resource
- ( Resource
- , resourceCreatedAt
- , resourceEditedAt
- , resourceDeletedAt
- , Status(..)
- , statuses
- , groupByStatus
- , statusDuring
- ) where
-
-import Data.Map (Map)
-import qualified Data.Map as M
-import Data.Maybe (fromMaybe)
-import Data.Time.Clock (UTCTime)
-
-class Resource a where
- resourceCreatedAt :: a -> UTCTime
- resourceEditedAt :: a -> Maybe UTCTime
- resourceDeletedAt :: a -> Maybe UTCTime
-
-data Status =
- Created
- | Edited
- | Deleted
- deriving (Eq, Show, Read, Ord, Enum, Bounded)
-
-statuses :: [Status]
-statuses = [minBound..]
-
-groupByStatus :: Resource a => UTCTime -> UTCTime -> [a] -> Map Status [a]
-groupByStatus start end resources =
- foldl
- (\m resource ->
- case statusDuring start end resource of
- Just status -> M.insertWith (++) status [resource] m
- Nothing -> m
- )
- M.empty
- resources
-
-statusDuring :: Resource a => UTCTime -> UTCTime -> a -> Maybe Status
-statusDuring start end resource
- | created && not deleted = Just Created
- | not created && edited && not deleted = Just Edited
- | not created && deleted = Just Deleted
- | otherwise = Nothing
- where
- created = belongs (resourceCreatedAt resource) start end
- edited = fromMaybe False (fmap (\t -> belongs t start end) $ resourceEditedAt resource)
- deleted = fromMaybe False (fmap (\t -> belongs t start end) $ resourceDeletedAt resource)
-
-belongs :: UTCTime -> UTCTime -> UTCTime -> Bool
-belongs time start end = time >= start && time < end
diff --git a/server/src/Secure.hs b/server/src/Secure.hs
deleted file mode 100644
index a30941f..0000000
--- a/server/src/Secure.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Secure
- ( loggedAction
- ) where
-
-import Control.Monad.IO.Class (liftIO)
-import qualified Data.Text.Lazy as TL
-import qualified Network.HTTP.Types.Status as HTTP
-import Web.Scotty
-
-import Common.Model (User)
-import qualified Common.Msg as Msg
-
-import qualified LoginSession
-import qualified Model.Query as Query
-import qualified Persistence.User as UserPersistence
-
-loggedAction :: (User -> ActionM ()) -> ActionM ()
-loggedAction action = do
- maybeToken <- LoginSession.get
- case maybeToken of
- Just token -> do
- maybeUser <- liftIO . Query.run . UserPersistence.get $ token
- case maybeUser of
- Just user ->
- action user
- Nothing -> do
- status HTTP.forbidden403
- html . TL.fromStrict . Msg.get $ Msg.Secure_Unauthorized
- Nothing -> do
- status HTTP.forbidden403
- html . TL.fromStrict . Msg.get $ Msg.Secure_Forbidden
diff --git a/server/src/SendMail.hs b/server/src/SendMail.hs
deleted file mode 100644
index 13d4072..0000000
--- a/server/src/SendMail.hs
+++ /dev/null
@@ -1,66 +0,0 @@
-module SendMail
- ( sendMail
- ) where
-
-import Control.Arrow (left)
-import Control.Exception (SomeException, try)
-import Data.Either (isLeft)
-import qualified Network.Mail.Mime as M
-
-import Data.Text (Text)
-import qualified Data.Text as T
-import qualified Data.Text.IO as T
-import qualified Data.Text.Lazy as LT
-import Data.Text.Lazy.Builder (fromText, toLazyText)
-
-import Conf (Conf)
-import qualified Conf
-import Model.Mail (Mail (..))
-
-sendMail :: Conf -> Mail -> IO (Either Text ())
-sendMail conf mail =
- if Conf.devMode conf
- then
- do
- T.putStrLn . mockMailMessage $ mail
- return (Right ())
- else
- do
- result <- left (T.pack . show) <$> (try (M.renderSendMail . getMimeMail $ mail) :: IO (Either SomeException ()))
- if isLeft result
- then putStrLn ("Error sending the following email:" ++ (show mail) ++ "\n" ++ (show result))
- else return ()
- return result
-
-mockMailMessage :: Mail -> Text
-mockMailMessage mail = T.concat $
- [ "[MOCK MAIL] "
- , subject mail
- , " (from: "
- , from mail
- , ") (to: "
- , T.intercalate ", " $ to mail
- , ")"
- , "\n"
- , body mail
- , "\n"
- ]
-
-getMimeMail :: Mail -> M.Mail
-getMimeMail (Mail mailFrom mailTo mailSubject mailPlainBody) =
- let fromMail = M.emptyMail (address mailFrom)
- in fromMail
- { M.mailTo = map address mailTo
- , M.mailParts = [ [ M.plainPart . strictToLazy $ mailPlainBody ] ]
- , M.mailHeaders = [("Subject", mailSubject)]
- }
-
-address :: Text -> M.Address
-address addressEmail =
- M.Address
- { M.addressName = Nothing
- , M.addressEmail = addressEmail
- }
-
-strictToLazy :: Text -> LT.Text
-strictToLazy = toLazyText . fromText
diff --git a/server/src/Statistics.hs b/server/src/Statistics.hs
deleted file mode 100644
index e463aac..0000000
--- a/server/src/Statistics.hs
+++ /dev/null
@@ -1,59 +0,0 @@
-module Statistics
- ( paymentsAndIncomes
- ) where
-
-import Control.Arrow ((&&&))
-import qualified Data.List as L
-import Data.Map (Map)
-import qualified Data.Map as M
-import qualified Data.Maybe as Maybe
-import qualified Data.Time.Calendar as Calendar
-
-import Common.Model (Income (..), MonthStats (..), Payment (..),
- Stats)
-
-paymentsAndIncomes :: [Payment] -> [Income] -> Stats
-paymentsAndIncomes payments incomes =
-
- map toMonthStat . M.toList $ foldl
- (\m p -> M.alter (alter p) (startOfMonth $ _payment_date p) m)
- M.empty
- payments
-
- where
-
- toMonthStat (start, paymentsByCategory) =
- MonthStats start paymentsByCategory (incomesAt start)
-
- incomesAt day =
- M.map (incomeAt day) lastToFirstIncomesByUser
-
- incomeAt day lastToFirstIncome =
- Maybe.maybe 0 _income_amount
- . Maybe.listToMaybe
- . dropWhile (\i -> _income_date i > day)
- $ lastToFirstIncome
-
- lastToFirstIncomesByUser =
- M.map (reverse . L.sortOn _income_date)
- . groupBy _income_userId
- $ incomes
-
- initMonthStats =
- M.fromList
- . map (\category -> (category, 0))
- . L.nub
- $ map _payment_category payments
-
- alter p Nothing = Just (addPayment p initMonthStats)
- alter p (Just monthStats) = Just (addPayment p monthStats)
-
- addPayment p monthStats = M.adjust ((+) (_payment_cost p)) (_payment_category p) monthStats
-
- startOfMonth day =
- let (y, m, _) = Calendar.toGregorian day
- in Calendar.fromGregorian y m 1
-
-groupBy :: Ord k => (a -> k) -> [a] -> Map k [a]
-groupBy key =
- M.fromListWith (++) . map (key &&& pure)
diff --git a/server/src/Util/Time.hs b/server/src/Util/Time.hs
deleted file mode 100644
index 4a29fcc..0000000
--- a/server/src/Util/Time.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module Util.Time
- ( belongToCurrentMonth
- , belongToCurrentWeek
- ) where
-
-import Data.Time.Calendar (toGregorian)
-import Data.Time.Calendar.WeekDate (toWeekDate)
-import Data.Time.Clock (UTCTime, getCurrentTime)
-
-import qualified Common.Util.Time as Time
-
-belongToCurrentMonth :: UTCTime -> IO Bool
-belongToCurrentMonth time = do
- (timeYear, timeMonth, _) <- toGregorian <$> Time.timeToDay time
- (actualYear, actualMonth, _) <- toGregorian <$> (getCurrentTime >>= Time.timeToDay)
- return (actualYear == timeYear && actualMonth == timeMonth)
-
-belongToCurrentWeek :: UTCTime -> IO Bool
-belongToCurrentWeek time = do
- (timeYear, timeWeek, _) <- toWeekDate <$> Time.timeToDay time
- (actualYear, actualWeek, _) <- toWeekDate <$> (getCurrentTime >>= Time.timeToDay)
- return (actualYear == timeYear && actualWeek == timeWeek)
diff --git a/server/src/Validation/Category.hs b/server/src/Validation/Category.hs
deleted file mode 100644
index 12f2117..0000000
--- a/server/src/Validation/Category.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Validation.Category
- ( createCategory
- , editCategory
- ) where
-
-import Data.Text (Text)
-import Data.Validation (Validation)
-import qualified Data.Validation as V
-
-import Common.Model (CreateCategoryForm (..),
- EditCategoryForm (..))
-import qualified Common.Validation.Category as CategoryValidation
-import Model.CreateCategory (CreateCategory (..))
-import Model.EditCategory (EditCategory (..))
-
-createCategory :: CreateCategoryForm -> Validation Text CreateCategory
-createCategory form =
- CreateCategory
- <$> CategoryValidation.name (_createCategoryForm_name form)
- <*> CategoryValidation.color (_createCategoryForm_color form)
-
-editCategory :: EditCategoryForm -> Validation Text EditCategory
-editCategory form =
- EditCategory
- <$> V.Success (_editCategoryForm_id form)
- <*> CategoryValidation.name (_editCategoryForm_name form)
- <*> CategoryValidation.color (_editCategoryForm_color form)
diff --git a/server/src/Validation/Income.hs b/server/src/Validation/Income.hs
deleted file mode 100644
index 5e034d1..0000000
--- a/server/src/Validation/Income.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Validation.Income
- ( createIncome
- , editIncome
- ) where
-
-import Data.Text (Text)
-import Data.Validation (Validation)
-import qualified Data.Validation as V
-
-import Common.Model (CreateIncomeForm (..),
- EditIncomeForm (..))
-import qualified Common.Validation.Income as IncomeValidation
-import Model.CreateIncome (CreateIncome (..))
-import Model.EditIncome (EditIncome (..))
-
-createIncome :: CreateIncomeForm -> Validation Text CreateIncome
-createIncome form =
- CreateIncome
- <$> IncomeValidation.amount (_createIncomeForm_amount form)
- <*> IncomeValidation.date (_createIncomeForm_date form)
-
-editIncome :: EditIncomeForm -> Validation Text EditIncome
-editIncome form =
- EditIncome
- <$> V.Success (_editIncomeForm_id form)
- <*> IncomeValidation.amount (_editIncomeForm_amount form)
- <*> IncomeValidation.date (_editIncomeForm_date form)
diff --git a/server/src/Validation/Payment.hs b/server/src/Validation/Payment.hs
deleted file mode 100644
index 20e370e..0000000
--- a/server/src/Validation/Payment.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Validation.Payment
- ( createPayment
- , editPayment
- ) where
-
-import Data.Text (Text)
-import Data.Validation (Validation)
-import qualified Data.Validation as V
-
-import Common.Model (CategoryId, CreatePaymentForm (..),
- EditPaymentForm (..))
-import qualified Common.Validation.Payment as PaymentValidation
-import Model.CreatePayment (CreatePayment (..))
-import Model.EditPayment (EditPayment (..))
-
-createPayment :: [CategoryId] -> CreatePaymentForm -> Validation Text CreatePayment
-createPayment categories form =
- CreatePayment
- <$> PaymentValidation.name (_createPaymentForm_name form)
- <*> PaymentValidation.cost (_createPaymentForm_cost form)
- <*> PaymentValidation.date (_createPaymentForm_date form)
- <*> PaymentValidation.category categories (_createPaymentForm_category form)
- <*> V.Success (_createPaymentForm_frequency form)
-
-editPayment :: [CategoryId] -> EditPaymentForm -> Validation Text EditPayment
-editPayment categories form =
- EditPayment
- <$> V.Success (_editPaymentForm_id form)
- <*> PaymentValidation.name (_editPaymentForm_name form)
- <*> PaymentValidation.cost (_editPaymentForm_cost form)
- <*> PaymentValidation.date (_editPaymentForm_date form)
- <*> PaymentValidation.category categories (_editPaymentForm_category form)
- <*> V.Success (_editPaymentForm_frequency form)
diff --git a/server/src/Validation/SignIn.hs b/server/src/Validation/SignIn.hs
deleted file mode 100644
index dc86122..0000000
--- a/server/src/Validation/SignIn.hs
+++ /dev/null
@@ -1,16 +0,0 @@
-module Validation.SignIn
- ( signIn
- ) where
-
-import Data.Text (Text)
-import Data.Validation (Validation)
-
-import Common.Model (SignInForm (..))
-import qualified Common.Validation.SignIn as SignInValidation
-import Model.SignIn (SignIn (..))
-
-signIn :: SignInForm -> Validation Text SignIn
-signIn form =
- SignIn
- <$> SignInValidation.email (_signInForm_email form)
- <*> SignInValidation.password (_signInForm_password form)
diff --git a/server/src/View/Mail/WeeklyReport.hs b/server/src/View/Mail/WeeklyReport.hs
deleted file mode 100644
index 3fe224f..0000000
--- a/server/src/View/Mail/WeeklyReport.hs
+++ /dev/null
@@ -1,124 +0,0 @@
-module View.Mail.WeeklyReport
- ( mail
- ) where
-
-import Data.List (sortOn)
-import Data.Map (Map)
-import qualified Data.Map as M
-import Data.Maybe (catMaybes, fromMaybe)
-import Data.Monoid ((<>))
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Time.Clock (UTCTime)
-
-import Common.Model (ExceedingPayer (..), Income (..),
- Payment (..), User (..), UserId)
-import qualified Common.Model as CM
-import qualified Common.Msg as Msg
-import qualified Common.View.Format as Format
-
-import Conf (Conf)
-import qualified Conf as Conf
-import Model.IncomeResource (IncomeResource (..))
-import Model.Mail (Mail (Mail))
-import qualified Model.Mail as M
-import Model.PaymentResource (PaymentResource (..))
-import qualified Payer as Payer
-import Resource (Status (..), groupByStatus, statuses)
-
-mail :: Conf -> [User] -> [Income] -> [Payment] -> Map UserId Int -> Map UserId Int -> Map UserId Int -> UTCTime -> UTCTime -> Mail
-mail conf users weekIncomes weekPayments cumulativeIncome preIncomeRepartition postIncomeRepartition start end =
- Mail
- { M.from = Conf.noReplyMail conf
- , M.to = map _user_email users
- , M.subject = T.concat
- [ Msg.get Msg.App_Title
- , " − "
- , Msg.get Msg.WeeklyReport_Title
- ]
- , M.body = body conf users weekIncomes weekPayments cumulativeIncome preIncomeRepartition postIncomeRepartition start end
- }
-
-body :: Conf -> [User] -> [Income] -> [Payment] -> Map UserId Int -> Map UserId Int -> Map UserId Int -> UTCTime -> UTCTime -> Text
-body conf users weekIncomes weekPayments cumulativeIncome preIncomeRepartition postIncomeRepartition start end =
- T.intercalate "\n" $
- [ exceedingPayers conf users cumulativeIncome preIncomeRepartition postIncomeRepartition
- , operations conf users paymentsGroupedByStatus incomesGroupedByStatus
- ]
- where
- paymentsGroupedByStatus = groupByStatus start end . map PaymentResource $ weekPayments
- incomesGroupedByStatus = groupByStatus start end . map IncomeResource $ weekIncomes
-
-exceedingPayers :: Conf -> [User] -> Map UserId Int -> Map UserId Int -> Map UserId Int -> Text
-exceedingPayers conf users cumulativeIncome preIncomeRepartition postIncomeRepartition =
- T.intercalate "\n" . map formatPayer $ payers
- where
- payers = Payer.getExceedingPayers users cumulativeIncome preIncomeRepartition postIncomeRepartition
- formatPayer p = T.concat
- [ " * "
- , fromMaybe "" $ _user_name <$> CM.findUser (_exceedingPayer_userId p) users
- , " + "
- , Format.price (Conf.currency conf) $ _exceedingPayer_amount p
- , "\n"
- ]
-
-operations :: Conf -> [User] -> Map Status [PaymentResource] -> Map Status [IncomeResource] -> Text
-operations conf users paymentsByStatus incomesByStatus =
- if M.null paymentsByStatus && M.null incomesByStatus
- then
- Msg.get Msg.WeeklyReport_Empty
- else
- T.intercalate "\n" . catMaybes . concat $
- [ map (\s -> paymentSection s conf users <$> M.lookup s paymentsByStatus) statuses
- , map (\s -> incomeSection s conf users <$> M.lookup s incomesByStatus) statuses
- ]
-
-paymentSection :: Status -> Conf -> [User] -> [PaymentResource] -> Text
-paymentSection status conf users payments =
- section sectionTitle sectionItems
- where count = length payments
- sectionTitle = Msg.get $ case status of
- Created -> if count > 1 then Msg.WeeklyReport_PaymentsCreated count else Msg.WeeklyReport_PaymentCreated count
- Edited -> if count > 1 then Msg.WeeklyReport_PaymentsEdited count else Msg.WeeklyReport_PaymentEdited count
- Deleted -> if count > 1 then Msg.WeeklyReport_PaymentsDeleted count else Msg.WeeklyReport_PaymentDeleted count
- sectionItems = map (payedFor status conf users) . sortOn _payment_date . map (\(PaymentResource p) -> p) $ payments
-
-payedFor :: Status -> Conf -> [User] -> Payment -> Text
-payedFor status conf users payment =
- case status of
- Deleted -> Msg.get (Msg.WeeklyReport_PayedForNot name amount for at)
- _ -> Msg.get (Msg.WeeklyReport_PayedFor name amount for at)
- where name = formatUserName (_payment_user payment) users
- amount = Format.price (Conf.currency conf) . _payment_cost $ payment
- for = _payment_name payment
- at = Format.longDay $ _payment_date payment
-
-incomeSection :: Status -> Conf -> [User] -> [IncomeResource] -> Text
-incomeSection status conf users incomes =
- section sectionTitle sectionItems
- where count = length incomes
- sectionTitle = Msg.get $ case status of
- Created -> if count > 1 then Msg.WeeklyReport_IncomesCreated count else Msg.WeeklyReport_IncomeCreated count
- Edited -> if count > 1 then Msg.WeeklyReport_IncomesEdited count else Msg.WeeklyReport_IncomeEdited count
- Deleted -> if count > 1 then Msg.WeeklyReport_IncomesDeleted count else Msg.WeeklyReport_IncomeDeleted count
- sectionItems = map (isPayedFrom status conf users) . sortOn _income_date . map (\(IncomeResource i) -> i) $ incomes
-
-isPayedFrom :: Status -> Conf -> [User] -> Income -> Text
-isPayedFrom status conf users income =
- case status of
- Deleted -> Msg.get (Msg.WeeklyReport_PayedFromNot name amount for)
- _ -> Msg.get (Msg.WeeklyReport_PayedFrom name amount for)
- where name = formatUserName (_income_userId income) users
- amount = Format.price (Conf.currency conf) . _income_amount $ income
- for = Format.longDay $ _income_date income
-
-formatUserName :: UserId -> [User] -> Text
-formatUserName userId = fromMaybe "−" . fmap _user_name . CM.findUser userId
-
-section :: Text -> [Text] -> Text
-section title items =
- T.concat
- [ title
- , "\n\n"
- , T.unlines . map (" * " <>) $ items
- ]
diff --git a/server/src/View/Page.hs b/server/src/View/Page.hs
deleted file mode 100644
index ae7a266..0000000
--- a/server/src/View/Page.hs
+++ /dev/null
@@ -1,43 +0,0 @@
-module View.Page
- ( page
- ) where
-
-import Data.Aeson (encode)
-import qualified Data.Aeson.Types as Json
-import Data.Text.Internal.Lazy (Text)
-import Data.Text.Lazy.Encoding (decodeUtf8)
-import Prelude hiding (init)
-
-import Text.Blaze.Html
-import Text.Blaze.Html.Renderer.Text (renderHtml)
-import Text.Blaze.Html5
-import qualified Text.Blaze.Html5 as H
-import Text.Blaze.Html5.Attributes
-import qualified Text.Blaze.Html5.Attributes as A
-
-import Common.Model (Init)
-import qualified Common.Msg as Msg
-
-page :: Maybe Init -> Text
-page init =
- renderHtml . docTypeHtml $ do
- H.head $ do
- meta ! charset "UTF-8"
- meta ! name "viewport" ! content "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
- H.title (toHtml $ Msg.get Msg.App_Title)
- script ! src "/javascript/main.js" $ ""
- script ! src "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.js" $ ""
- jsonScript "init" init
- link ! rel "stylesheet" ! type_ "text/css" ! href "/css/reset.css"
- link ! rel "stylesheet" ! type_ "text/css" ! href "/css/main.css"
- link ! rel "icon" ! type_ "image/png" ! href "/images/icon.png"
- H.body $ do
- H.div ! A.class_ "spinner" $ ""
-
-
-jsonScript :: Json.ToJSON a => Text -> a -> Html
-jsonScript scriptId json =
- script
- ! A.id (toValue scriptId)
- ! type_ "application/json"
- $ toHtml . decodeUtf8 . encode $ json
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..796a8df
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,30 @@
+with (import (builtins.fetchGit {
+ name = "nixpkgs-20.09";
+ url = "git@github.com:nixos/nixpkgs.git";
+ rev = "cd63096d6d887d689543a0b97743d28995bc9bc3";
+ ref = "refs/tags/20.09";
+}){});
+
+let nixpkgs-mozilla = fetchFromGitHub {
+ owner = "mozilla";
+ repo = "nixpkgs-mozilla";
+ # commit from 2020-10-28
+ rev = "8c007b60731c07dd7a052cce508de3bb1ae849b4";
+ sha256 = "1zybp62zz0h077zm2zmqs2wcg3whg6jqaah9hcl1gv4x8af4zhs6";
+}; in
+
+with import "${nixpkgs-mozilla.out}/rust-overlay.nix" pkgs pkgs;
+
+pkgs.mkShell {
+
+ buildInputs = [
+ # rustChannels.nightly.rust
+ ((rustChannelOf { channel = "1.49.0"; }).rust)
+ cargo-watch
+ lld
+ openssl
+ pkgconfig
+ sqlite
+ ];
+
+}
diff --git a/sql/fixtures.sql b/sql/fixtures.sql
new file mode 100644
index 0000000..61ff934
--- /dev/null
+++ b/sql/fixtures.sql
@@ -0,0 +1,45 @@
+INSERT INTO
+ users(email, name, password)
+VALUES
+ ('john@mail.com', 'John', '$2b$10$Qy8lqrTqHdzwLZwsqvO09eMwehA.vti.AGwPVj/pZYL94Ni6zozT2'),
+ ('lisa@mail.com', 'Lisa', '$2b$10$Qy8lqrTqHdzwLZwsqvO09eMwehA.vti.AGwPVj/pZYL94Ni6zozT2');
+
+INSERT INTO
+ incomes(user_id, date, amount)
+VALUES
+ (1, '2020-01-01', 1500),
+ (2, '2020-01-01', 2000);
+
+INSERT INTO
+ categories(name, color)
+VALUES
+ ('Habitation', '#aa0000'),
+ ('Alimentation', '#00aa00'),
+ ('Animaux', '#0000aa');
+
+INSERT INTO
+ payments(user_id, name, cost, date, frequency, category_id)
+VALUES
+ (1, 'Loyer', 600, '2021-01-01', 'Punctual', 1),
+ (2, 'Loyer', 600, '2020-02-01', 'Punctual', 1),
+ (2, 'Loyer', 600, '2020-03-01', 'Punctual', 1),
+ (2, 'Loyer', 600, '2020-04-01', 'Punctual', 1),
+ (2, 'Loyer', 600, '2020-05-01', 'Punctual', 1),
+ (2, 'Loyer', 600, '2020-06-01', 'Punctual', 1),
+ (1, 'Loyer', 600, '2020-07-01', 'Punctual', 1),
+ (1, 'Loyer', 600, '2020-08-01', 'Punctual', 1),
+ (2, 'Loyer', 600, '2020-09-01', 'Punctual', 1),
+ (2, 'Loyer', 600, '2020-10-01', 'Punctual', 1),
+ (2, 'Loyer', 600, '2020-11-01', 'Punctual', 1),
+ (1, 'Loyer', 600, '2020-12-01', 'Punctual', 1),
+ (1, 'Loyer', 600, '2020-01-01', 'Punctual', 1),
+ (1, 'Marché', 55, '2021-01-02', 'Punctual', 2),
+ (2, 'Restaurant', 60, '2020-12-10', 'Punctual', 2),
+ (1, 'Vétérinaire', 105, '2020-11-18', 'Punctual', 3),
+ (1, 'Magasin', 150, '2020-10-29', 'Punctual', 2);
+
+INSERT INTO
+ jobs(name)
+VALUES
+ ('MonthlyPayment'),
+ ('WeeklyReport');
diff --git a/server/migrations/1.sql b/sql/migrations/1.sql
index d7c300e..d7c300e 100644
--- a/server/migrations/1.sql
+++ b/sql/migrations/1.sql
diff --git a/server/migrations/2.sql b/sql/migrations/2.sql
index c1d502f..c1d502f 100644
--- a/server/migrations/2.sql
+++ b/sql/migrations/2.sql
diff --git a/server/migrations/3.sql b/sql/migrations/3.sql
index a3d8a13..a3d8a13 100644
--- a/server/migrations/3.sql
+++ b/sql/migrations/3.sql
diff --git a/sql/migrations/4.sql b/sql/migrations/4.sql
new file mode 100644
index 0000000..ec386cb
--- /dev/null
+++ b/sql/migrations/4.sql
@@ -0,0 +1,91 @@
+-- Payments
+
+CREATE TABLE IF NOT EXISTS "payments"(
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL REFERENCES "users",
+ "name" TEXT NOT NULL,
+ "cost" INTEGER NOT NULL,
+ "date" DATE NOT NULL,
+ "frequency" TEXT NOT NULL,
+ "category_id" INTEGER NOT NULL REFERENCES "categories",
+ "created_at" DATE NULL DEFAULT (datetime('now')),
+ "updated_at" DATE NULL,
+ "deleted_at" DATE NULL
+);
+
+INSERT INTO payments (id, user_id, name, cost, date, frequency, category_id, created_at, updated_at, deleted_at)
+ SELECT id, user_id, name, cost, date, frequency, category, created_at, edited_at, deleted_at
+ FROM payment;
+
+DROP TABLE payment;
+
+CREATE INDEX payment_date ON payments(date);
+
+-- Categories
+
+CREATE TABLE IF NOT EXISTS "categories"(
+ "id" INTEGER PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "color" TEXT NOT NULL,
+ "created_at" DATE NULL DEFAULT (datetime('now')),
+ "updated_at" DATE NULL,
+ "deleted_at" DATE NULL
+);
+
+INSERT INTO categories (id, name, color, created_at, updated_at, deleted_at)
+ SELECT id, name, color, created_at, edited_at, deleted_at
+ FROM category;
+
+DROP TABLE category;
+
+-- Users
+
+CREATE TABLE IF NOT EXISTS "users"(
+ "id" INTEGER PRIMARY KEY,
+ "email" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "password" TEXT NOT NULL,
+ "login_token" TEXT NULL,
+ "created_at" DATE NULL DEFAULT (datetime('now')),
+ "updated_at" DATE NULL,
+ "deleted_at" DATE NULL,
+ CONSTRAINT "uniq_user_email" UNIQUE ("email"),
+ CONSTRAINT "uniq_user_name" UNIQUE ("name")
+);
+
+INSERT INTO users (id, created_at, email, name, password, login_token)
+ SELECT id, creation, email, name, password, sign_in_token
+ FROM user;
+
+DROP TABLE user;
+
+-- Jobs
+
+CREATE TABLE IF NOT EXISTS "jobs"(
+ "name" TEXT PRIMARY KEY,
+ "last_execution" DATE NOT NULL DEFAULT (datetime('now'))
+);
+
+INSERT INTO jobs (name, last_execution)
+ SELECT kind, last_execution
+ FROM job;
+
+DROP TABLE job;
+
+-- Incomes
+
+CREATE TABLE IF NOT EXISTS "incomes"(
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL REFERENCES "users",
+ "date" DATE NOT NULL,
+ "amount" INTEGER NOT NULL,
+ "created_at" DATE NULL DEFAULT (datetime('now')),
+ "updated_at" DATE NULL,
+ "deleted_at" DATE NULL
+);
+
+INSERT INTO incomes (id, user_id, date, amount, created_at, updated_at, deleted_at)
+ SELECT id, user_id, date, amount, created_at, edited_at, deleted_at
+ FROM income;
+
+DROP TABLE income;
diff --git a/src/assets.rs b/src/assets.rs
new file mode 100644
index 0000000..dc46c78
--- /dev/null
+++ b/src/assets.rs
@@ -0,0 +1,27 @@
+use sha2::{Digest, Sha256};
+use std::collections::HashMap;
+use std::fs;
+use std::iter::FromIterator;
+
+pub fn get() -> HashMap<String, String> {
+ let paths = fs::read_dir("assets").unwrap().map(|e| {
+ let path = format!("{}", e.unwrap().path().display());
+ let file = fs::read(&path).unwrap();
+ let mut path_iter = path.split("/");
+ path_iter.next();
+ let name = path_iter.collect::<Vec<&str>>().join("/");
+ let hashed = format!("/assets/{}/{}", sha256(file), name);
+ (name, hashed)
+ });
+ HashMap::from_iter(paths)
+}
+
+fn sha256(input: Vec<u8>) -> String {
+ let mut hasher = Sha256::new();
+ hasher.update(input);
+ hasher
+ .finalize()
+ .iter()
+ .map(|b| format!("{:x}", b))
+ .collect()
+}
diff --git a/src/controller/balance.rs b/src/controller/balance.rs
new file mode 100644
index 0000000..228ff04
--- /dev/null
+++ b/src/controller/balance.rs
@@ -0,0 +1,71 @@
+use hyper::{Body, Response};
+use std::collections::HashMap;
+use tera::Context;
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+use crate::db;
+use crate::model::user::User;
+use crate::payer;
+use crate::templates;
+
+pub async fn get(wallet: &Wallet) -> Response<Body> {
+ let users = db::users::list(&wallet.pool).await;
+
+ let incomes_from = db::incomes::defined_for_all(&wallet.pool).await;
+ let user_incomes = match incomes_from {
+ Some(from) => db::incomes::cumulative(&wallet.pool, from).await,
+ None => HashMap::new(),
+ };
+ let template_user_incomes =
+ get_template_user_incomes(&users, &user_incomes);
+ let total_income: i64 = user_incomes.values().sum();
+
+ let user_payments = db::payments::repartition(&wallet.pool).await;
+ let template_user_payments =
+ get_template_user_payments(&users, &user_payments);
+ let total_payments: i64 =
+ user_payments.clone().into_iter().map(|p| p.1).sum();
+
+ let exceeding_payers =
+ payer::exceeding(&users, &user_incomes, &user_payments);
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Balance);
+ context.insert("connected_user", &wallet.user);
+ context.insert(
+ "incomes_from",
+ &incomes_from.map(|d| d.format("%d/%m/%Y").to_string()),
+ );
+ context.insert("total_income", &total_income);
+ context.insert("user_incomes", &template_user_incomes);
+ context.insert("total_payments", &total_payments);
+ context.insert("user_payments", &template_user_payments);
+ context.insert("exceeding_payers", &exceeding_payers);
+
+ utils::template(&wallet.assets, &wallet.templates, "balance.html", context)
+}
+
+fn get_template_user_payments(
+ users: &Vec<User>,
+ user_payments: &HashMap<i64, i64>,
+) -> Vec<(String, i64)> {
+ let mut user_payments: Vec<(String, i64)> = users
+ .into_iter()
+ .map(|u| (u.name.clone(), *user_payments.get(&u.id).unwrap_or(&0)))
+ .collect();
+ user_payments.sort_by_key(|i| i.1);
+ user_payments
+}
+
+fn get_template_user_incomes(
+ users: &Vec<User>,
+ user_incomes: &HashMap<i64, i64>,
+) -> Vec<(String, i64)> {
+ let mut user_incomes: Vec<(String, i64)> = users
+ .into_iter()
+ .map(|u| (u.name.clone(), *user_incomes.get(&u.id).unwrap_or(&0)))
+ .collect();
+ user_incomes.sort_by_key(|i| i.1);
+ user_incomes
+}
diff --git a/src/controller/categories.rs b/src/controller/categories.rs
new file mode 100644
index 0000000..b1a3664
--- /dev/null
+++ b/src/controller/categories.rs
@@ -0,0 +1,141 @@
+use hyper::{Body, Response};
+use std::collections::HashMap;
+use tera::Context;
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+use crate::db;
+use crate::queries;
+use crate::templates;
+use crate::validation;
+
+pub async fn table(
+ wallet: &Wallet,
+ query: queries::Categories,
+) -> Response<Body> {
+ let categories = db::categories::list(&wallet.pool).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Categories);
+ context.insert("connected_user", &wallet.user);
+ context.insert("categories", &categories);
+ context.insert("highlight", &query.highlight);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "category/table.html",
+ context,
+ )
+}
+
+pub async fn create_form(wallet: &Wallet) -> Response<Body> {
+ create_form_feedback(wallet, HashMap::new(), None).await
+}
+
+async fn create_form_feedback(
+ wallet: &Wallet,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Categories);
+ context.insert("connected_user", &wallet.user.clone());
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "category/create.html",
+ context,
+ )
+}
+
+pub async fn create(
+ wallet: &Wallet,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ create_form_feedback(wallet, form.clone(), Some(e.to_string()))
+ };
+
+ match validation::category::create(&form) {
+ Some(category) => {
+ match db::categories::create(&wallet.pool, &category).await {
+ Some(id) => {
+ utils::redirect(&format!("/categories?highlight={}", id))
+ }
+ None => error("Erreur serveur").await,
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn update_form(id: i64, wallet: &Wallet) -> Response<Body> {
+ update_form_feedback(id, wallet, HashMap::new(), None).await
+}
+
+async fn update_form_feedback(
+ id: i64,
+ wallet: &Wallet,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let category = db::categories::get(&wallet.pool, id).await;
+ let is_category_used =
+ db::payments::is_category_used(&wallet.pool, id).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Categories);
+ context.insert("connected_user", &wallet.user);
+ context.insert("id", &id);
+ context.insert("category", &category);
+ context.insert("is_category_used", &is_category_used);
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "category/update.html",
+ context,
+ )
+}
+
+pub async fn update(
+ id: i64,
+ wallet: &Wallet,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ update_form_feedback(id, wallet, form.clone(), Some(e.to_string()))
+ };
+
+ match validation::category::update(&form) {
+ Some(update_category) => {
+ if db::categories::update(&wallet.pool, id, &update_category).await
+ {
+ utils::redirect(&format!("/categories?highlight={}", id))
+ } else {
+ error("Erreur serveur").await
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn delete(id: i64, wallet: &Wallet) -> Response<Body> {
+ if db::categories::delete(&wallet.pool, id).await {
+ utils::redirect("/categories")
+ } else {
+ update_form_feedback(
+ id,
+ wallet,
+ HashMap::new(),
+ Some("Erreur serveur".to_string()),
+ )
+ .await
+ }
+}
diff --git a/src/controller/error.rs b/src/controller/error.rs
new file mode 100644
index 0000000..8dad16b
--- /dev/null
+++ b/src/controller/error.rs
@@ -0,0 +1,31 @@
+use hyper::header::CACHE_CONTROL;
+use hyper::{Body, Response};
+use std::collections::HashMap;
+use tera::{Context, Tera};
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+
+pub fn error(wallet: &Wallet, title: &str, message: &str) -> Response<Body> {
+ utils::with_header(
+ Response::new(
+ template(&wallet.assets, &wallet.templates, title, message).into(),
+ ),
+ CACHE_CONTROL,
+ "no-cache",
+ )
+}
+
+pub fn template(
+ assets: &HashMap<String, String>,
+ templates: &Tera,
+ title: &str,
+ message: &str,
+) -> String {
+ let mut context = Context::new();
+ context.insert("title", title);
+ context.insert("message", message);
+ context.insert("assets", assets);
+
+ templates.render("error.html", &context).unwrap()
+}
diff --git a/src/controller/incomes.rs b/src/controller/incomes.rs
new file mode 100644
index 0000000..ea7f1cf
--- /dev/null
+++ b/src/controller/incomes.rs
@@ -0,0 +1,221 @@
+use chrono::Datelike;
+use chrono::Utc;
+use hyper::{Body, Response};
+use std::collections::HashMap;
+use tera::Context;
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+use crate::db;
+use crate::queries;
+use crate::templates;
+use crate::validation;
+
+static PER_PAGE: i64 = 10;
+
+pub async fn table(wallet: &Wallet, query: queries::Incomes) -> Response<Body> {
+ let page = query.page.unwrap_or(1);
+ let count = db::incomes::count(&wallet.pool).await;
+ let incomes = db::incomes::list(&wallet.pool, page, PER_PAGE).await;
+ let max_page = (count as f32 / PER_PAGE as f32).ceil() as i64;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Incomes);
+ context.insert("connected_user", &wallet.user);
+ context.insert("incomes", &incomes);
+ context.insert("page", &page);
+ context.insert("max_page", &max_page);
+ context.insert("highlight", &query.highlight);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "income/table.html",
+ context,
+ )
+}
+
+static MONTHS: [&str; 12] = [
+ "Janvier",
+ "Février",
+ "Mars",
+ "Avril",
+ "Mai",
+ "Juin",
+ "Juillet",
+ "Août",
+ "Septembre",
+ "Octobre",
+ "Novembre",
+ "Décembre",
+];
+
+pub async fn create_form(
+ wallet: &Wallet,
+ query: queries::Incomes,
+) -> Response<Body> {
+ create_form_feedback(wallet, query, HashMap::new(), None).await
+}
+
+async fn create_form_feedback(
+ wallet: &Wallet,
+ query: queries::Incomes,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let users = db::users::list(&wallet.pool).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Incomes);
+ context.insert("connected_user", &wallet.user);
+ context.insert("users", &users);
+ context.insert("query", &query);
+ context.insert("current_month", &Utc::today().naive_utc().month());
+ context.insert("months", &MONTHS);
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "income/create.html",
+ context,
+ )
+}
+
+pub async fn create(
+ wallet: &Wallet,
+ query: queries::Incomes,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ create_form_feedback(wallet, query, form.clone(), Some(e.to_string()))
+ };
+
+ match validation::income::create(&form) {
+ Some(income) => {
+ if !db::incomes::defined_at(
+ &wallet.pool,
+ income.user_id,
+ income.month,
+ income.year,
+ )
+ .await
+ .is_empty()
+ {
+ error("Un revenu est déjà défini à cette date.").await
+ } else {
+ match db::incomes::create(&wallet.pool, &income).await {
+ Some(id) => {
+ let row = db::incomes::get_row(&wallet.pool, id).await;
+ let page = (row - 1) / PER_PAGE + 1;
+ utils::redirect(&format!(
+ "/incomes?page={}&highlight={}",
+ page, id
+ ))
+ }
+ None => error("Erreur serveur").await,
+ }
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn update_form(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Incomes,
+) -> Response<Body> {
+ update_form_feedback(id, wallet, query, HashMap::new(), None).await
+}
+
+async fn update_form_feedback(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Incomes,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let users = db::users::list(&wallet.pool).await;
+ let income = db::incomes::get(&wallet.pool, id).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Incomes);
+ context.insert("connected_user", &wallet.user);
+ context.insert("users", &users);
+ context.insert("id", &id);
+ context.insert("income", &income);
+ context.insert("query", &query);
+ context.insert("months", &MONTHS);
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "income/update.html",
+ context,
+ )
+}
+
+pub async fn update(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Incomes,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ update_form_feedback(
+ id,
+ wallet,
+ query,
+ form.clone(),
+ Some(e.to_string()),
+ )
+ };
+
+ match validation::income::update(&form) {
+ Some(income) => {
+ let existing_incomes = db::incomes::defined_at(
+ &wallet.pool,
+ income.user_id,
+ income.month,
+ income.year,
+ )
+ .await;
+ if existing_incomes.into_iter().any(|eid| eid != id) {
+ error("Un revenu est déjà défini à cette date.").await
+ } else if db::incomes::update(&wallet.pool, id, &income).await {
+ let row = db::incomes::get_row(&wallet.pool, id).await;
+ let page = (row - 1) / PER_PAGE + 1;
+ utils::redirect(&format!(
+ "/incomes?page={}&highlight={}",
+ page, id
+ ))
+ } else {
+ error("Erreur serveur").await
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn delete(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Incomes,
+) -> Response<Body> {
+ if db::incomes::delete(&wallet.pool, id).await {
+ utils::redirect(&format!("/incomes?page={}", query.page.unwrap_or(1)))
+ } else {
+ update_form_feedback(
+ id,
+ wallet,
+ query,
+ HashMap::new(),
+ Some("Erreur serveur".to_string()),
+ )
+ .await
+ }
+}
diff --git a/src/controller/login.rs b/src/controller/login.rs
new file mode 100644
index 0000000..ea9db57
--- /dev/null
+++ b/src/controller/login.rs
@@ -0,0 +1,86 @@
+use bcrypt;
+use hyper::{Body, Response};
+use sqlx::sqlite::SqlitePool;
+use std::collections::HashMap;
+use tera::{Context, Tera};
+use uuid::Uuid;
+
+use crate::controller::wallet::Wallet;
+use crate::controller::{error, utils};
+use crate::db;
+use crate::model::config::Config;
+use crate::model::user::User;
+use crate::validation;
+
+pub async fn page(
+ assets: &HashMap<String, String>,
+ templates: &Tera,
+ error: Option<String>,
+) -> Response<Body> {
+ let connected_user: Option<User> = None;
+
+ let mut context = Context::new();
+ context.insert("connected_user", &connected_user);
+ context.insert("error", &error);
+
+ utils::template(assets, templates, "login.html", context)
+}
+
+pub async fn login(
+ config: Config,
+ assets: &HashMap<String, String>,
+ templates: &Tera,
+ form: HashMap<String, String>,
+ pool: SqlitePool,
+) -> Response<Body> {
+ let not_authorized = page(
+ assets,
+ templates,
+ Some("Vous n’êtes pas autorisé à vous connecter.".to_string()),
+ )
+ .await;
+ let server_error =
+ page(assets, templates, Some("Erreur serveur.".to_string())).await;
+ match validation::login::login(&form) {
+ Some(login) => {
+ match db::users::get_password_hash(&pool, login.email.clone()).await
+ {
+ Some(hash) => match bcrypt::verify(login.password, &hash) {
+ Ok(true) => {
+ let login_token = Uuid::new_v4();
+ if db::users::set_login_token(
+ &pool,
+ login.email,
+ login_token.clone().to_string(),
+ )
+ .await
+ {
+ utils::with_login_cookie(
+ config,
+ login_token,
+ utils::redirect("/"),
+ )
+ } else {
+ server_error
+ }
+ }
+ Ok(false) => not_authorized,
+ Err(err) => {
+ error!("Error verifying bcrypt password: {:?}", err);
+ server_error
+ }
+ },
+ None => not_authorized,
+ }
+ }
+ None => not_authorized,
+ }
+}
+
+pub async fn logout(config: Config, wallet: &Wallet) -> Response<Body> {
+ if db::users::remove_login_token(&wallet.pool, wallet.user.id).await {
+ utils::with_logout_cookie(config, utils::redirect("/"))
+ } else {
+ error::error(&wallet, "Erreur serveur", "Erreur serveur")
+ }
+}
diff --git a/src/controller/mod.rs b/src/controller/mod.rs
new file mode 100644
index 0000000..e2ef561
--- /dev/null
+++ b/src/controller/mod.rs
@@ -0,0 +1,9 @@
+pub mod balance;
+pub mod categories;
+pub mod error;
+pub mod incomes;
+pub mod login;
+pub mod payments;
+pub mod statistics;
+pub mod utils;
+pub mod wallet;
diff --git a/src/controller/payments.rs b/src/controller/payments.rs
new file mode 100644
index 0000000..ab4bd92
--- /dev/null
+++ b/src/controller/payments.rs
@@ -0,0 +1,227 @@
+use hyper::header::CONTENT_TYPE;
+use hyper::{Body, Response};
+use std::collections::HashMap;
+use tera::Context;
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+use crate::db;
+use crate::model::frequency::Frequency;
+use crate::queries;
+use crate::templates;
+use crate::validation;
+
+static PER_PAGE: i64 = 10;
+
+pub async fn table(
+ wallet: &Wallet,
+ query: queries::Payments,
+) -> Response<Body> {
+ let page = query.page.unwrap_or(1);
+ let count = db::payments::count(&wallet.pool, &query).await;
+ let payments =
+ db::payments::list_for_table(&wallet.pool, &query, PER_PAGE).await;
+ let max_page = (count.count as f32 / PER_PAGE as f32).ceil() as i64;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Payments);
+ context.insert("connected_user", &wallet.user);
+ context.insert("payments", &payments);
+ context.insert("page", &page);
+ context.insert("max_page", &max_page);
+ context.insert("query", &query);
+ context.insert("count", &count.count);
+ context.insert("total_cost", &count.total_cost);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "payment/table.html",
+ context,
+ )
+}
+
+pub async fn create_form(
+ wallet: &Wallet,
+ query: queries::Payments,
+) -> Response<Body> {
+ create_form_feedback(wallet, query, HashMap::new(), None).await
+}
+
+async fn create_form_feedback(
+ wallet: &Wallet,
+ query: queries::Payments,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let users = db::users::list(&wallet.pool).await;
+ let categories = db::categories::list(&wallet.pool).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Payments);
+ context.insert("connected_user", &wallet.user);
+ context.insert("users", &users);
+ context.insert("categories", &categories);
+ context.insert("query", &query);
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "payment/create.html",
+ context,
+ )
+}
+
+pub async fn create(
+ wallet: &Wallet,
+ query: queries::Payments,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ create_form_feedback(wallet, query, form.clone(), Some(e.to_string()))
+ };
+
+ match validation::payment::create(&form) {
+ Some(create_payment) => {
+ match db::payments::create(&wallet.pool, &create_payment).await {
+ Some(id) => {
+ let row = db::payments::get_row(
+ &wallet.pool,
+ id,
+ create_payment.frequency,
+ )
+ .await;
+ let page = (row - 1) / PER_PAGE + 1;
+ let query = queries::Payments {
+ page: Some(page),
+ search: None,
+ frequency: Some(create_payment.frequency),
+ highlight: Some(id),
+ };
+ utils::redirect(&format!(
+ "/{}",
+ queries::payments_url(query)
+ ))
+ }
+ None => error("Erreur serveur.").await,
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn update_form(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Payments,
+) -> Response<Body> {
+ update_form_feedback(id, wallet, query, HashMap::new(), None).await
+}
+
+async fn update_form_feedback(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Payments,
+ form: HashMap<String, String>,
+ error: Option<String>,
+) -> Response<Body> {
+ let payment = db::payments::get_for_form(&wallet.pool, id).await;
+ let users = db::users::list(&wallet.pool).await;
+ let categories = db::categories::list(&wallet.pool).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Payments);
+ context.insert("connected_user", &wallet.user);
+ context.insert("id", &id);
+ context.insert("payment", &payment);
+ context.insert("users", &users);
+ context.insert("categories", &categories);
+ context.insert("query", &query);
+ context.insert("form", &form);
+ context.insert("error", &error);
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "payment/update.html",
+ context,
+ )
+}
+
+pub async fn update(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Payments,
+ form: HashMap<String, String>,
+) -> Response<Body> {
+ let error = |e: &str| {
+ update_form_feedback(
+ id,
+ wallet,
+ query.clone(),
+ form.clone(),
+ Some(e.to_string()),
+ )
+ };
+
+ match validation::payment::update(&form) {
+ Some(update_payment) => {
+ if db::payments::update(&wallet.pool, id, &update_payment).await {
+ let frequency = query.frequency.unwrap_or(Frequency::Punctual);
+ let row =
+ db::payments::get_row(&wallet.pool, id, frequency).await;
+ let page = (row - 1) / PER_PAGE + 1;
+ let query = queries::Payments {
+ page: Some(page),
+ search: None,
+ frequency: Some(frequency),
+ highlight: Some(id),
+ };
+ utils::redirect(&format!("/{}", queries::payments_url(query)))
+ } else {
+ error("Erreur serveur").await
+ }
+ }
+ None => error("Erreur lors de la validation du formulaire.").await,
+ }
+}
+
+pub async fn delete(
+ id: i64,
+ wallet: &Wallet,
+ query: queries::Payments,
+) -> Response<Body> {
+ if db::payments::delete(&wallet.pool, id).await {
+ let query = queries::Payments {
+ highlight: None,
+ ..query
+ };
+ utils::redirect(&format!("/{}", queries::payments_url(query)))
+ } else {
+ update_form_feedback(
+ id,
+ wallet,
+ query,
+ HashMap::new(),
+ Some("Erreur serveur".to_string()),
+ )
+ .await
+ }
+}
+
+pub async fn search_category(
+ wallet: &Wallet,
+ query: queries::PaymentCategory,
+) -> Response<Body> {
+ match db::payments::search_category(&wallet.pool, query.payment_name).await
+ {
+ Some(category_id) => utils::with_header(
+ Response::new(format!("{}", category_id).into()),
+ CONTENT_TYPE,
+ "application/json",
+ ),
+ None => utils::not_found(),
+ }
+}
diff --git a/src/controller/statistics.rs b/src/controller/statistics.rs
new file mode 100644
index 0000000..38a5787
--- /dev/null
+++ b/src/controller/statistics.rs
@@ -0,0 +1,30 @@
+use hyper::{Body, Response};
+use tera::Context;
+
+use crate::controller::utils;
+use crate::controller::wallet::Wallet;
+use crate::db;
+use crate::templates;
+
+pub async fn get(wallet: &Wallet) -> Response<Body> {
+ let categories = db::categories::list(&wallet.pool).await;
+ let payments = db::payments::list_for_stats(&wallet.pool).await;
+ let incomes = db::incomes::total_each_month(&wallet.pool).await;
+
+ let mut context = Context::new();
+ context.insert("header", &templates::Header::Statistics);
+ context.insert("connected_user", &wallet.user);
+ context.insert(
+ "json_categories",
+ &serde_json::to_string(&categories).unwrap(),
+ );
+ context.insert("json_payments", &serde_json::to_string(&payments).unwrap());
+ context.insert("json_incomes", &serde_json::to_string(&incomes).unwrap());
+
+ utils::template(
+ &wallet.assets,
+ &wallet.templates,
+ "statistics.html",
+ context,
+ )
+}
diff --git a/src/controller/utils.rs b/src/controller/utils.rs
new file mode 100644
index 0000000..225f8a4
--- /dev/null
+++ b/src/controller/utils.rs
@@ -0,0 +1,119 @@
+use hyper::header::{
+ HeaderName, HeaderValue, CACHE_CONTROL, CONTENT_TYPE, LOCATION, SET_COOKIE,
+};
+use hyper::{Body, Response, StatusCode};
+use std::collections::HashMap;
+use tera::{Context, Tera};
+use tokio::fs::File;
+use tokio_util::codec::{BytesCodec, FramedRead};
+use uuid::Uuid;
+
+use crate::controller::error;
+use crate::model::config::Config;
+
+pub fn with_header(
+ response: Response<Body>,
+ name: HeaderName,
+ value: &str,
+) -> Response<Body> {
+ with_headers(response, vec![(name, value)])
+}
+
+pub fn with_headers(
+ response: Response<Body>,
+ headers: Vec<(HeaderName, &str)>,
+) -> Response<Body> {
+ let mut response = response;
+ let response_headers = response.headers_mut();
+ for (name, value) in headers {
+ response_headers.insert(name, HeaderValue::from_str(value).unwrap());
+ }
+ response
+}
+
+pub fn with_login_cookie(
+ config: Config,
+ login_token: Uuid,
+ response: Response<Body>,
+) -> Response<Body> {
+ let cookie = format!(
+ "TOKEN={}; SameSite=Strict; HttpOnly; Max-Age=86400{}",
+ login_token,
+ if config.secure_cookies {
+ "; Secure"
+ } else {
+ ""
+ }
+ );
+
+ with_header(response, SET_COOKIE, &cookie)
+}
+
+pub fn with_logout_cookie(
+ config: Config,
+ response: Response<Body>,
+) -> Response<Body> {
+ let cookie = format!(
+ "TOKEN=; SameSite=Strict; HttpOnly; Max-Age=0{}",
+ if config.secure_cookies {
+ "; Secure"
+ } else {
+ ""
+ }
+ );
+
+ with_header(response, SET_COOKIE, &cookie)
+}
+
+pub fn template(
+ assets: &HashMap<String, String>,
+ templates: &Tera,
+ path: &str,
+ context: Context,
+) -> Response<Body> {
+ let mut context = context.clone();
+ context.insert("assets", assets);
+
+ let response = match templates.render(path, &context) {
+ Ok(template) => Response::new(template.into()),
+ Err(err) => Response::new(
+ error::template(
+ assets,
+ templates,
+ "Erreur serveur",
+ &format!(
+ "Erreur lors de la préparation de la page : {:?}",
+ err
+ ),
+ )
+ .into(),
+ ),
+ };
+
+ with_headers(
+ response,
+ vec![(CONTENT_TYPE, "text/html"), (CACHE_CONTROL, "no-cache")],
+ )
+}
+
+pub fn redirect(uri: &str) -> Response<Body> {
+ let mut response = Response::default();
+ *response.status_mut() = StatusCode::MOVED_PERMANENTLY;
+ with_headers(response, vec![(LOCATION, uri), (CACHE_CONTROL, "no-cache")])
+}
+
+pub fn not_found() -> Response<Body> {
+ let mut response = Response::default();
+ *response.status_mut() = StatusCode::NOT_FOUND;
+ response
+}
+
+pub async fn file(filename: &str) -> Response<Body> {
+ if let Ok(file) = File::open(filename).await {
+ let stream = FramedRead::new(file, BytesCodec::new());
+ let body = Body::wrap_stream(stream);
+ with_header(Response::new(body), CACHE_CONTROL, "max-age=3153600000")
+ } else {
+ not_found()
+ }
+}
diff --git a/src/controller/wallet.rs b/src/controller/wallet.rs
new file mode 100644
index 0000000..2a4a593
--- /dev/null
+++ b/src/controller/wallet.rs
@@ -0,0 +1,13 @@
+use sqlx::sqlite::SqlitePool;
+use std::collections::HashMap;
+use tera::Tera;
+
+use crate::model::user::User;
+
+#[derive(Clone)]
+pub struct Wallet {
+ pub pool: SqlitePool,
+ pub assets: HashMap<String, String>,
+ pub templates: Tera,
+ pub user: User,
+}
diff --git a/src/db/categories.rs b/src/db/categories.rs
new file mode 100644
index 0000000..05b1323
--- /dev/null
+++ b/src/db/categories.rs
@@ -0,0 +1,132 @@
+use sqlx::sqlite::SqlitePool;
+
+use crate::model::category::{Category, Create, Update};
+
+pub async fn list(pool: &SqlitePool) -> Vec<Category> {
+ let res = sqlx::query_as::<_, Category>(
+ r#"
+SELECT
+ id,
+ name,
+ color
+FROM
+ categories
+WHERE
+ deleted_at IS NULL
+ORDER BY
+ name
+ "#,
+ )
+ .fetch_all(pool)
+ .await;
+
+ match res {
+ Ok(categories) => categories,
+ Err(err) => {
+ error!("Error listing categories: {:?}", err);
+ vec![]
+ }
+ }
+}
+
+pub async fn get(pool: &SqlitePool, id: i64) -> Option<Category> {
+ let query = r#"
+SELECT
+ id,
+ name,
+ color
+FROM
+ categories
+WHERE
+ id = ?
+ AND deleted_at IS NULL
+ "#;
+
+ let res = sqlx::query_as::<_, Category>(query)
+ .bind(id)
+ .fetch_one(pool)
+ .await;
+
+ match res {
+ Ok(p) => Some(p),
+ Err(err) => {
+ error!("Error looking for category {}: {:?}", id, err);
+ None
+ }
+ }
+}
+
+pub async fn create(pool: &SqlitePool, c: &Create) -> Option<i64> {
+ let res = sqlx::query(
+ r#"
+INSERT INTO
+ categories(name, color, created_at)
+VALUES
+ (?, ?, datetime())
+ "#,
+ )
+ .bind(c.name.clone())
+ .bind(c.color.clone())
+ .execute(pool)
+ .await;
+
+ match res {
+ Ok(x) => Some(x.last_insert_rowid()),
+ Err(err) => {
+ error!("Error creating category: {:?}", err);
+ None
+ }
+ }
+}
+
+pub async fn update(pool: &SqlitePool, id: i64, c: &Update) -> bool {
+ let res = sqlx::query(
+ r#"
+UPDATE
+ categories
+SET
+ name = ?,
+ color = ?,
+ updated_at = datetime()
+WHERE
+ id = ?
+ "#,
+ )
+ .bind(c.name.clone())
+ .bind(c.color.clone())
+ .bind(id)
+ .execute(pool)
+ .await;
+
+ match res {
+ Ok(_) => true,
+ Err(err) => {
+ error!("Error updating category {}: {:?}", id, err);
+ false
+ }
+ }
+}
+
+pub async fn delete(pool: &SqlitePool, id: i64) -> bool {
+ let res = sqlx::query(
+ r#"
+UPDATE
+ categories
+SET
+ deleted_at = datetime()
+WHERE
+ id = ?
+ "#,
+ )
+ .bind(id)
+ .execute(pool)
+ .await;
+
+ match res {
+ Ok(_) => true,
+ Err(err) => {
+ error!("Error deleting category {}: {:?}", id, err);
+ false
+ }
+ }
+}
diff --git a/src/db/incomes.rs b/src/db/incomes.rs
new file mode 100644
index 0000000..cbbfce7
--- /dev/null
+++ b/src/db/incomes.rs
@@ -0,0 +1,494 @@
+use chrono::NaiveDate;
+use sqlx::error::Error;
+use sqlx::sqlite::{SqlitePool, SqliteRow};
+use sqlx_core::row::Row;
+use std::collections::HashMap;
+use std::iter::FromIterator;
+
+use crate::model::income::{Create, Form, Stat, Table, Update};
+use crate::model::report::Report;
+
+pub async fn count(pool: &SqlitePool) -> i64 {
+ let query = r#"
+SELECT
+ COUNT(*) AS count
+FROM
+ incomes
+WHERE
+ incomes.deleted_at IS NULL
+ "#;
+
+ let res = sqlx::query(&query)
+ .map(|row: SqliteRow| row.get("count"))
+ .fetch_one(pool)
+ .await;
+
+ match res {
+ Ok(count) => count,
+ Err(err) => {
+ error!("Error counting incomes: {:?}", err);
+ 0
+ }
+ }
+}
+
+pub async fn list(pool: &SqlitePool, page: i64, per_page: i64) -> Vec<Table> {
+ let query = r#"
+SELECT
+ incomes.id,
+ users.name AS user,
+ strftime('%m/%Y', incomes.date) AS date,
+ incomes.amount
+FROM
+ incomes
+INNER JOIN
+ users
+ON
+ incomes.user_id = users.id
+WHERE
+ incomes.deleted_at IS NULL
+ORDER BY
+ incomes.date DESC
+LIMIT ?
+OFFSET ?
+ "#;
+
+ let res = sqlx::query_as::<_, Table>(query)
+ .bind(per_page)
+ .bind((page - 1) * per_page)
+ .fetch_all(pool)
+ .await;
+
+ match res {
+ Ok(incomes) => incomes,
+ Err(err) => {
+ error!("Error listing incomes: {:?}", err);
+ vec![]
+ }
+ }
+}
+
+pub async fn get_row(pool: &SqlitePool, id: i64) -> i64 {
+ let query = r#"
+SELECT
+ row
+FROM (
+ SELECT
+ ROW_NUMBER () OVER (ORDER BY date DESC) AS row,
+ id
+ FROM
+ incomes
+ WHERE
+ deleted_at IS NULL
+)
+WHERE
+ id = ?
+ "#;
+
+ let res = sqlx::query(query)
+ .bind(id)
+ .map(|row: SqliteRow| row.get("row"))
+ .fetch_one(pool)
+ .await;
+
+ match res {
+ Ok(count) => count,
+ Err(err) => {
+ error!("Error getting income row: {:?}", err);
+ 1
+ }
+ }
+}
+
+pub async fn get(pool: &SqlitePool, id: i64) -> Option<Form> {
+ let query = r#"
+SELECT
+ id,
+ amount,
+ user_id,
+ CAST(strftime('%m', date) AS INTEGER) as month,
+ CAST(strftime('%Y', date) AS INTEGER) as year
+FROM
+ incomes
+WHERE
+ id = ?
+ AND deleted_at IS NULL
+ "#;
+
+ let res = sqlx::query_as::<_, Form>(query)
+ .bind(id)
+ .fetch_one(pool)
+ .await;
+
+ match res {
+ Ok(p) => Some(p),
+ Err(err) => {
+ error!("Error looking for income {}: {:?}", id, err);
+ None
+ }
+ }
+}
+
+pub async fn create(pool: &SqlitePool, i: &Create) -> Option<i64> {
+ let res = sqlx::query(
+ r#"
+INSERT INTO
+ incomes(user_id, date, amount, created_at)
+VALUES
+ (?, ?, ?, datetime())
+ "#,
+ )
+ .bind(i.user_id)
+ .bind(NaiveDate::from_ymd(i.year, i.month, 1))
+ .bind(i.amount)
+ .execute(pool)
+ .await;
+
+ match res {
+ Ok(x) => Some(x.last_insert_rowid()),
+ Err(err) => {
+ error!("Error creating income: {:?}", err);
+ None
+ }
+ }
+}
+
+pub async fn defined_at(
+ pool: &SqlitePool,
+ user_id: i64,
+ month: u32,
+ year: i32,
+) -> Vec<i64> {
+ let query = r#"
+SELECT
+ id
+FROM
+ incomes
+WHERE
+ user_id = ?
+ AND date = ?
+ AND deleted_at IS NULL
+ "#;
+
+ let res = sqlx::query(&query)
+ .bind(user_id)
+ .bind(NaiveDate::from_ymd(year, month, 1))
+ .map(|row: SqliteRow| row.get("id"))
+ .fetch_all(pool)
+ .await;
+
+ match res {
+ Ok(ids) => ids,
+ Err(Error::RowNotFound) => vec![],
+ Err(err) => {
+ error!("Error looking if income is defined: {:?}", err);
+ vec![]
+ }
+ }
+}
+
+pub async fn update(pool: &SqlitePool, id: i64, i: &Update) -> bool {
+ let res = sqlx::query(
+ r#"
+UPDATE
+ incomes
+SET
+ user_id = ?,
+ date = ?,
+ amount = ?,
+ updated_at = datetime()
+WHERE
+ id = ?
+ "#,
+ )
+ .bind(i.user_id)
+ .bind(NaiveDate::from_ymd(i.year, i.month, 1))
+ .bind(i.amount)
+ .bind(id)
+ .execute(pool)
+ .await;
+
+ match res {
+ Ok(_) => true,
+ Err(err) => {
+ error!("Error updating income {}: {:?}", id, err);
+ false
+ }
+ }
+}
+
+pub async fn delete(pool: &SqlitePool, id: i64) -> bool {
+ let res = sqlx::query(
+ r#"
+UPDATE
+ incomes
+SET
+ deleted_at = datetime()
+WHERE
+ id = ?
+ "#,
+ )
+ .bind(id)
+ .execute(pool)
+ .await;
+
+ match res {
+ Ok(_) => true,
+ Err(err) => {
+ error!("Error deleting income {}: {:?}", id, err);
+ false
+ }
+ }
+}
+
+pub async fn defined_for_all(pool: &SqlitePool) -> Option<NaiveDate> {
+ let res = sqlx::query(
+ r#"
+SELECT
+ (CASE COUNT(users.id) == COUNT(min_income.date)
+ WHEN 1 THEN MIN(min_income.date)
+ ELSE NULL
+ END) AS date
+FROM
+ users
+LEFT OUTER JOIN (
+ SELECT
+ user_id,
+ MIN(date) AS date
+ FROM
+ incomes
+ WHERE
+ deleted_at IS NULL
+ GROUP BY
+ user_id
+) min_income
+ON
+ users.id = min_income.user_id;
+ "#,
+ )
+ .map(|row: SqliteRow| row.get("date"))
+ .fetch_one(pool)
+ .await;
+
+ match res {
+ Ok(d) => d,
+ Err(err) => {
+ error!("Error looking for incomes defined for all: {:?}", err);
+ None
+ }
+ }
+}
+
+pub async fn cumulative(
+ pool: &SqlitePool,
+ from: NaiveDate,
+) -> HashMap<i64, i64> {
+ let res = sqlx::query(&cumulative_query(from))
+ .map(|row: SqliteRow| (row.get("user_id"), row.get("income")))
+ .fetch_all(pool)
+ .await;
+
+ match res {
+ Ok(incomes) => HashMap::from_iter(incomes),
+ Err(err) => {
+ error!("Error computing cumulative income: {:?}", err);
+ HashMap::new()
+ }
+ }
+}
+
+/// Select cumulative income of users from the given date and until now.
+///
+/// Associate each month income to its start and end bounds,
+/// then compute the total income of each period,
+/// sum it to get the final result.
+///
+/// Considering each month to be 365 / 12 days long.
+fn cumulative_query(from: NaiveDate) -> String {
+ format!(
+ r#"
+SELECT
+ users.id AS user_id,
+ COALESCE(incomes.income, 0) AS income
+FROM
+ users
+LEFT OUTER JOIN (
+ SELECT
+ user_id,
+ CAST(ROUND(SUM(count)) AS INTEGER) AS income
+ FROM (
+ SELECT
+ I1.user_id,
+ ((JULIANDAY(MIN(I2.date)) - JULIANDAY(I1.date)) * I1.amount * 12 / 365) AS count
+ FROM
+ ({}) AS I1
+ INNER JOIN
+ ({}) AS I2
+ ON
+ I2.date > I1.date
+ AND I2.user_id == I1.user_id
+ GROUP BY
+ I1.date, I1.user_id
+ )
+ GROUP BY
+ user_id
+) incomes
+ON
+ users.id = incomes.user_id
+ "#,
+ bounded_query(">".to_string(), from.format("%Y-%m-%d").to_string()),
+ bounded_query("<".to_string(), "date()".to_string())
+ )
+}
+
+/// Select bounded incomes to the operator and date.
+///
+/// It filters incomes according to the operator and date,
+/// and adds the income at this date.
+fn bounded_query(op: String, date: String) -> String {
+ format!(
+ r#"
+SELECT
+ user_id,
+ date,
+ amount
+FROM (
+ SELECT
+ user_id,
+ {} AS date,
+ amount,
+ MAX(date) AS max_date
+ FROM
+ incomes
+ WHERE
+ date <= {}
+ AND deleted_at IS NULL
+ GROUP BY
+ user_id
+) UNION
+SELECT
+ user_id,
+ date,
+ amount
+FROM
+ incomes
+WHERE
+ date {} {}
+ AND deleted_at IS NULL
+ "#,
+ date, date, op, date
+ )
+}
+
+/// Select total income each month.
+///
+/// For each month, from the first defined income and until now,
+/// compute the total income of the users.
+pub async fn total_each_month(pool: &SqlitePool) -> Vec<Stat> {
+ let query = r#"
+WITH RECURSIVE dates(date) AS (
+ VALUES((
+ SELECT
+ strftime('%Y-%m-01', MIN(date))
+ FROM
+ incomes
+ WHERE
+ deleted_at IS NULL
+ ))
+ UNION ALL
+ SELECT
+ date(date, '+1 month')
+ FROM
+ dates
+ WHERE
+ date < date(date(), '-1 month')
+)
+SELECT
+ strftime('%Y-%m-01', dates.date) AS date,
+ (
+ SELECT
+ SUM(amount) AS amount
+ FROM (
+ SELECT (
+ SELECT
+ amount
+ FROM
+ incomes
+ WHERE
+ user_id = users.id
+ AND date < date(dates.date, '+1 month')
+ AND deleted_at IS NULL
+ ORDER BY
+ date DESC
+ LIMIT
+ 1
+ ) AS amount
+ FROM
+ users
+ )
+ ) AS amount
+FROM
+ dates;
+ "#;
+
+ let res = sqlx::query_as::<_, Stat>(query).fetch_all(pool).await;
+
+ match res {
+ Ok(xs) => xs,
+ Err(err) => {
+ error!("Error listing incomes for statistics: {:?}", err);
+ vec![]
+ }
+ }
+}
+
+pub async fn last_week(pool: &SqlitePool) -> Vec<Report> {
+ let query = r#"
+SELECT
+ strftime('%m/%Y', incomes.date) AS date,
+ users.name AS name,
+ incomes.amount AS amount,
+ (CASE
+ WHEN
+ incomes.deleted_at IS NOT NULL
+ THEN
+ 'Deleted'
+ WHEN
+ incomes.updated_at IS NOT NULL
+ AND incomes.created_at < date('now', 'weekday 0', '-13 days')
+ THEN
+ 'Updated'
+ ELSE
+ 'Created'
+ END) AS action
+FROM
+ incomes
+INNER JOIN
+ users
+ON
+ incomes.user_id = users.id
+WHERE
+ (
+ incomes.created_at >= date('now', 'weekday 0', '-13 days')
+ AND incomes.created_at < date('now', 'weekday 0', '-6 days')
+ ) OR (
+ incomes.updated_at >= date('now', 'weekday 0', '-13 days')
+ AND incomes.updated_at < date('now', 'weekday 0', '-6 days')
+ ) OR (
+ incomes.deleted_at >= date('now', 'weekday 0', '-13 days')
+ AND incomes.deleted_at < date('now', 'weekday 0', '-6 days')
+ )
+ORDER BY
+ incomes.date
+ "#;
+
+ let res = sqlx::query_as::<_, Report>(query).fetch_all(pool).await;
+
+ match res {
+ Ok(payments) => payments,
+ Err(err) => {
+ error!("Error listing payments for report: {:?}", err);
+ vec![]
+ }
+ }
+}
diff --git a/src/db/jobs.rs b/src/db/jobs.rs
new file mode 100644
index 0000000..88c2005
--- /dev/null
+++ b/src/db/jobs.rs
@@ -0,0 +1,56 @@
+use sqlx::error::Error;
+use sqlx::sqlite::SqlitePool;
+
+use crate::model::job::Job;
+
+pub async fn should_run(pool: &SqlitePool, job: Job) -> bool {
+ let run_from = match job {
+ Job::WeeklyReport => "date('now', 'weekday 0', '-6 days')",
+ Job::MonthlyPayment => "date('now', 'start of month')",
+ };
+
+ let query = format!(
+ r#"
+SELECT
+ 1
+FROM
+ jobs
+WHERE
+ name = ?
+ AND last_execution < {}
+ "#,
+ run_from
+ );
+
+ let res = sqlx::query(&query).bind(job).fetch_one(pool).await;
+
+ match res {
+ Ok(_) => true,
+ Err(Error::RowNotFound) => false,
+ Err(err) => {
+ error!("Error looking if job should run: {:?}", err);
+ false
+ }
+ }
+}
+
+pub async fn actualize_last_execution(pool: &SqlitePool, job: Job) -> () {
+ let query = r#"
+UPDATE
+ jobs
+SET
+ last_execution = datetime()
+WHERE
+ name = ?
+ "#;
+
+ let res = sqlx::query(query).bind(job).execute(pool).await;
+
+ match res {
+ Ok(_) => (),
+ Err(err) => {
+ error!("Error actualizing job last execution: {:?}", err);
+ ()
+ }
+ }
+}
diff --git a/src/db/mod.rs b/src/db/mod.rs
new file mode 100644
index 0000000..a0aa3dc
--- /dev/null
+++ b/src/db/mod.rs
@@ -0,0 +1,6 @@
+pub mod categories;
+pub mod incomes;
+pub mod jobs;
+pub mod payments;
+pub mod users;
+mod utils;
diff --git a/src/db/payments.rs b/src/db/payments.rs
new file mode 100644
index 0000000..0197375
--- /dev/null
+++ b/src/db/payments.rs
@@ -0,0 +1,525 @@
+use sqlx::error::Error;
+use sqlx::sqlite::{Sqlite, SqliteArguments};
+use sqlx::sqlite::{SqlitePool, SqliteRow};
+use sqlx::FromRow;
+use sqlx_core::row::Row;
+use std::collections::HashMap;
+use std::iter::FromIterator;
+
+use crate::db::utils;
+use crate::model::frequency::Frequency;
+use crate::model::payment;
+use crate::model::report::Report;
+use crate::queries;
+use crate::utils::text;
+
+#[derive(FromRow)]
+pub struct Count {
+ pub count: i64,
+ pub total_cost: i64,
+}
+
+pub async fn count(
+ pool: &SqlitePool,
+ payment_query: &queries::Payments,
+) -> Count {
+ let search = payment_query.search.clone().unwrap_or("".to_string());
+
+ let query = format!(
+ r#"
+SELECT
+ COUNT(*) AS count,
+ SUM(payments.cost) AS total_cost
+FROM
+ payments
+INNER JOIN
+ users ON users.id = payments.user_id
+INNER JOIN
+ categories ON categories.id = payments.category_id
+WHERE
+ payments.deleted_at IS NULL
+ AND payments.frequency = ?
+ {}
+ "#,
+ search_query(search.clone())
+ );
+
+ let res = bind_search(
+ sqlx::query_as::<_, Count>(&query)
+ .bind(payment_query.frequency.unwrap_or(Frequency::Punctual)),
+ search,
+ )
+ .fetch_one(pool)
+ .await;
+
+ match res {
+ Ok(count) => count,
+ Err(err) => {
+ error!("Error counting payments: {:?}", err);
+ Count {
+ count: 0,
+ total_cost: 0,
+ }
+ }
+ }
+}
+
+pub async fn list_for_table(
+ pool: &SqlitePool,
+ payment_query: &queries::Payments,
+ per_page: i64,
+) -> Vec<payment::Table> {
+ let offset = (payment_query.page.unwrap_or(1) - 1) * per_page;
+ let search = payment_query.search.clone().unwrap_or("".to_string());
+
+ let query = format!(
+ r#"
+SELECT
+ payments.id,
+ payments.name,
+ payments.cost,
+ users.name AS user,
+ categories.name AS category_name,
+ categories.color AS category_color,
+ strftime('%d/%m/%Y', date) AS date,
+ payments.frequency AS frequency
+FROM
+ payments
+INNER JOIN
+ users ON users.id = payments.user_id
+INNER JOIN
+ categories ON categories.id = payments.category_id
+WHERE
+ payments.deleted_at IS NULL
+ AND payments.frequency = ?
+ {}
+ORDER BY
+ payments.date DESC
+LIMIT ?
+OFFSET ?
+ "#,
+ search_query(search.clone())
+ );
+
+ let res = bind_search(
+ sqlx::query_as::<_, payment::Table>(&query)
+ .bind(payment_query.frequency.unwrap_or(Frequency::Punctual)),
+ search,
+ )
+ .bind(per_page)
+ .bind(offset)
+ .fetch_all(pool)
+ .await;
+
+ match res {
+ Ok(payments) => payments,
+ Err(err) => {
+ error!("Error listing payments: {:?}", err);
+ vec![]
+ }
+ }
+}
+
+fn search_query(search: String) -> String {
+ let payments_name = utils::format_key_for_search("payments.name");
+ let users_name = utils::format_key_for_search("users.name");
+ let categories_name = utils::format_key_for_search("categories.name");
+
+ search
+ .split_ascii_whitespace()
+ .map(|_| {
+ format!(
+ r#"
+AND (
+ {} LIKE ?
+ OR payments.cost LIKE ?
+ OR {} LIKE ?
+ OR {} LIKE ?
+ OR strftime('%d/%m/%Y', date) LIKE ?
+)
+ "#,
+ payments_name, users_name, categories_name
+ )
+ })
+ .collect::<Vec<String>>()
+ .join(" ")
+}
+
+fn bind_search<'a, Row: FromRow<'a, SqliteRow>>(
+ query: sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>>,
+ search: String,
+) -> sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>> {
+ search.split_ascii_whitespace().fold(query, |q, word| {
+ let s = format!("%{}%", text::format_search(&word.to_string()));
+ q.bind(s.clone())
+ .bind(s.clone())
+ .bind(s.clone())
+ .bind(s.clone())
+ .bind(s.clone())
+ })
+}
+
+pub async fn list_for_stats(pool: &SqlitePool) -> Vec<payment::Stat> {
+ let query = r#"
+SELECT
+ strftime('%Y-%m-01', payments.date) AS start_date,
+ SUM(payments.cost) AS cost,
+ payments.category_id AS category_id
+FROM
+ payments
+WHERE
+ payments.deleted_at IS NULL
+ AND payments.frequency = 'Punctual'
+GROUP BY
+ start_date,
+ payments.category_id;
+ "#;
+
+ let result = sqlx::query_as::<_, payment::Stat>(query)
+ .fetch_all(pool)
+ .await;
+
+ match result {
+ Ok(payments) => payments,
+ Err(err) => {
+ error!("Error listing payments for statistics: {:?}", err);
+ vec![]
+ }
+ }
+}
+
+pub async fn get_row(pool: &SqlitePool, id: i64, frequency: Frequency) -> i64 {
+ let query = r#"
+SELECT
+ row
+FROM (
+ SELECT
+ ROW_NUMBER () OVER (ORDER BY date DESC) AS row,
+ id
+ FROM
+ payments
+ WHERE
+ deleted_at IS NULL
+ AND frequency = ?
+)
+WHERE
+ id = ?
+ "#;
+
+ let res = sqlx::query(query)
+ .bind(frequency)
+ .bind(id)
+ .map(|row: SqliteRow| row.get("row"))
+ .fetch_one(pool)
+ .await;
+
+ match res {
+ Ok(count) => count,
+ Err(err) => {
+ error!("Error getting payment row: {:?}", err);
+ 1
+ }
+ }
+}
+
+pub async fn get_for_form(pool: &SqlitePool, id: i64) -> Option<payment::Form> {
+ let query = r#"
+SELECT
+ id,
+ name,
+ cost,
+ user_id,
+ category_id,
+ strftime('%Y-%m-%d', date) AS date,
+ frequency AS frequency
+FROM
+ payments
+WHERE
+ id = ?
+ AND deleted_at IS NULL
+ "#;
+
+ let res = sqlx::query_as::<_, payment::Form>(query)
+ .bind(id)
+ .fetch_one(pool)
+ .await;
+
+ match res {
+ Ok(p) => Some(p),
+ Err(err) => {
+ error!("Error looking for payment {}: {:?}", id, err);
+ None
+ }
+ }
+}
+
+pub async fn create(pool: &SqlitePool, p: &payment::Create) -> Option<i64> {
+ let res = sqlx::query(
+ r#"
+INSERT INTO
+ payments(name, cost, user_id, category_id, date, frequency, created_at)
+VALUES
+ (?, ?, ?, ?, ?, ?, datetime())
+ "#,
+ )
+ .bind(p.name.clone())
+ .bind(p.cost)
+ .bind(p.user_id)
+ .bind(p.category_id)
+ .bind(p.date)
+ .bind(p.frequency)
+ .execute(pool)
+ .await;
+
+ match res {
+ Ok(x) => Some(x.last_insert_rowid()),
+ Err(err) => {
+ error!("Error creating payment: {:?}", err);
+ None
+ }
+ }
+}
+
+pub async fn update(pool: &SqlitePool, id: i64, p: &payment::Update) -> bool {
+ let res = sqlx::query(
+ r#"
+UPDATE
+ payments
+SET
+ name = ?,
+ cost = ?,
+ user_id = ?,
+ category_id = ?,
+ date = ?,
+ updated_at = datetime()
+WHERE
+ id = ?
+ "#,
+ )
+ .bind(p.name.clone())
+ .bind(p.cost)
+ .bind(p.user_id)
+ .bind(p.category_id)
+ .bind(p.date)
+ .bind(id)
+ .execute(pool)
+ .await;
+
+ match res {
+ Ok(_) => true,
+ Err(err) => {
+ error!("Error updating payment {}: {:?}", id, err);
+ false
+ }
+ }
+}
+
+pub async fn delete(pool: &SqlitePool, id: i64) -> bool {
+ let res = sqlx::query(
+ r#"
+UPDATE
+ payments
+SET
+ deleted_at = datetime()
+WHERE
+ id = ?
+ "#,
+ )
+ .bind(id)
+ .execute(pool)
+ .await;
+
+ match res {
+ Ok(_) => true,
+ Err(err) => {
+ error!("Error deleting payment {}: {:?}", id, err);
+ false
+ }
+ }
+}
+
+pub async fn search_category(
+ pool: &SqlitePool,
+ payment_name: String,
+) -> Option<i64> {
+ let query = format!(
+ r#"
+SELECT
+ category_id
+FROM
+ payments
+WHERE
+ deleted_at IS NULL
+ AND {} LIKE ?
+ORDER BY
+ updated_at, created_at
+ "#,
+ utils::format_key_for_search("name")
+ );
+
+ let res = sqlx::query(&query)
+ .bind(text::format_search(&format!("%{}%", payment_name)))
+ .map(|row: SqliteRow| row.get("category_id"))
+ .fetch_one(pool)
+ .await;
+
+ match res {
+ Ok(category) => Some(category),
+ Err(Error::RowNotFound) => None,
+ Err(err) => {
+ error!(
+ "Error looking for the category of {}: {:?}",
+ payment_name, err
+ );
+ None
+ }
+ }
+}
+
+pub async fn is_category_used(pool: &SqlitePool, category_id: i64) -> bool {
+ let query = r#"
+SELECT
+ 1
+FROM
+ payments
+WHERE
+ category_id = ?
+ AND deleted_at IS NULL
+LIMIT
+ 1
+ "#;
+
+ let res = sqlx::query(&query).bind(category_id).fetch_one(pool).await;
+
+ match res {
+ Ok(_) => true,
+ Err(Error::RowNotFound) => false,
+ Err(err) => {
+ error!(
+ "Error looking if category {} is used: {:?}",
+ category_id, err
+ );
+ false
+ }
+ }
+}
+
+pub async fn repartition(pool: &SqlitePool) -> HashMap<i64, i64> {
+ let query = r#"
+SELECT
+ users.id AS user_id,
+ COALESCE(payments.sum, 0) AS sum
+FROM
+ users
+LEFT OUTER JOIN (
+ SELECT
+ user_id,
+ SUM(cost) AS sum
+ FROM
+ payments
+ WHERE
+ deleted_at IS NULL
+ AND frequency = 'Punctual'
+ GROUP BY
+ user_id
+) payments
+ON
+ users.id = payments.user_id"#;
+
+ let res = sqlx::query(&query)
+ .map(|row: SqliteRow| (row.get("user_id"), row.get("sum")))
+ .fetch_all(pool)
+ .await;
+
+ match res {
+ Ok(costs) => HashMap::from_iter(costs),
+ Err(err) => {
+ error!("Error getting payments repartition: {:?}", err);
+ HashMap::new()
+ }
+ }
+}
+
+pub async fn create_monthly_payments(pool: &SqlitePool) -> () {
+ let query = r#"
+INSERT INTO
+ payments(name, cost, user_id, category_id, date, frequency, created_at)
+SELECT
+ name,
+ cost,
+ user_id,
+ category_id,
+ date() AS date,
+ 'Punctual' AS frequency,
+ datetime() AS created_at
+FROM
+ payments
+WHERE
+ frequency = 'Monthly'
+ AND deleted_at IS NULL
+ "#;
+
+ let res = sqlx::query(query).execute(pool).await;
+
+ match res {
+ Ok(_) => (),
+ Err(err) => {
+ error!("Error creating monthly payments: {:?}", err);
+ ()
+ }
+ }
+}
+
+pub async fn last_week(pool: &SqlitePool) -> Vec<Report> {
+ let query = r#"
+SELECT
+ strftime('%d/%m/%Y', payments.date) AS date,
+ (payments.name || ' (' || users.name || ')') AS name,
+ payments.cost AS amount,
+ (CASE
+ WHEN
+ payments.deleted_at IS NOT NULL
+ THEN
+ 'Deleted'
+ WHEN
+ payments.updated_at IS NOT NULL
+ AND payments.created_at < date('now', 'weekday 0', '-13 days')
+ THEN
+ 'Updated'
+ ELSE
+ 'Created'
+ END) AS action
+FROM
+ payments
+INNER JOIN
+ users
+ON
+ payments.user_id = users.id
+WHERE
+ payments.frequency = 'Punctual'
+ AND (
+ (
+ payments.created_at >= date('now', 'weekday 0', '-13 days')
+ AND payments.created_at < date('now', 'weekday 0', '-6 days')
+ ) OR (
+ payments.updated_at >= date('now', 'weekday 0', '-13 days')
+ AND payments.updated_at < date('now', 'weekday 0', '-6 days')
+ ) OR (
+ payments.deleted_at >= date('now', 'weekday 0', '-13 days')
+ AND payments.deleted_at < date('now', 'weekday 0', '-6 days')
+ )
+ )
+ORDER BY
+ payments.date
+ "#;
+
+ let res = sqlx::query_as::<_, Report>(query).fetch_all(pool).await;
+
+ match res {
+ Ok(payments) => payments,
+ Err(err) => {
+ error!("Error listing payments for report: {:?}", err);
+ vec![]
+ }
+ }
+}
diff --git a/src/db/users.rs b/src/db/users.rs
new file mode 100644
index 0000000..82326a9
--- /dev/null
+++ b/src/db/users.rs
@@ -0,0 +1,144 @@
+use sqlx::error::Error;
+use sqlx::sqlite::{SqlitePool, SqliteRow};
+use sqlx_core::row::Row;
+
+use crate::model::user::User;
+
+pub async fn list(pool: &SqlitePool) -> Vec<User> {
+ let res = sqlx::query_as::<_, User>(
+ r#"
+SELECT
+ id,
+ name,
+ email
+FROM
+ users
+ORDER BY
+ name
+ "#,
+ )
+ .fetch_all(pool)
+ .await;
+
+ match res {
+ Ok(users) => users,
+ Err(err) => {
+ error!("Error listing users: {:?}", err);
+ vec![]
+ }
+ }
+}
+
+pub async fn set_login_token(
+ pool: &SqlitePool,
+ email: String,
+ login_token: String,
+) -> bool {
+ let res = sqlx::query(
+ r#"
+UPDATE
+ users
+SET
+ login_token = ?,
+ updated_at = datetime()
+WHERE
+ email = ?
+ "#,
+ )
+ .bind(login_token)
+ .bind(email)
+ .execute(pool)
+ .await;
+
+ match res {
+ Ok(_) => true,
+ Err(err) => {
+ error!("Error updating login token: {:?}", err);
+ false
+ }
+ }
+}
+
+pub async fn remove_login_token(pool: &SqlitePool, id: i64) -> bool {
+ let res = sqlx::query(
+ r#"
+UPDATE
+ users
+SET
+ login_token = NULL,
+ updated_at = datetime()
+WHERE
+ id = ?
+ "#,
+ )
+ .bind(id)
+ .execute(pool)
+ .await;
+
+ match res {
+ Ok(_) => true,
+ Err(err) => {
+ error!("Error removing login token: {:?}", err);
+ false
+ }
+ }
+}
+
+pub async fn get_by_login_token(
+ pool: &SqlitePool,
+ login_token: String,
+) -> Option<User> {
+ let res = sqlx::query_as::<_, User>(
+ r#"
+SELECT
+ id,
+ name,
+ email
+FROM
+ users
+WHERE
+ login_token = ?
+ "#,
+ )
+ .bind(login_token)
+ .fetch_one(pool)
+ .await;
+
+ match res {
+ Ok(user) => Some(user),
+ Err(Error::RowNotFound) => None,
+ Err(err) => {
+ error!("Error getting user from login token: {:?}", err);
+ None
+ }
+ }
+}
+
+pub async fn get_password_hash(
+ pool: &SqlitePool,
+ email: String,
+) -> Option<String> {
+ let res = sqlx::query(
+ r#"
+SELECT
+ password
+FROM
+ users
+WHERE
+ email = ?
+ "#,
+ )
+ .bind(email)
+ .map(|row: SqliteRow| row.get("password"))
+ .fetch_one(pool)
+ .await;
+
+ match res {
+ Ok(hash) => Some(hash),
+ Err(Error::RowNotFound) => None,
+ Err(err) => {
+ error!("Error getting password hash: {:?}", err);
+ None
+ }
+ }
+}
diff --git a/src/db/utils.rs b/src/db/utils.rs
new file mode 100644
index 0000000..621a69c
--- /dev/null
+++ b/src/db/utils.rs
@@ -0,0 +1,3 @@
+pub fn format_key_for_search(value: &str) -> String {
+ format!("replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(lower({}), 'à', 'a'), 'â', 'a'), 'ç', 'c'), 'è', 'e'), 'é', 'e'), 'ê', 'e'), 'ë', 'e'), 'î', 'i'), 'ï', 'i'), 'ô', 'o'), 'ù', 'u'), 'û', 'u'), 'ü', 'u')", value)
+}
diff --git a/src/jobs/jobs.rs b/src/jobs/jobs.rs
new file mode 100644
index 0000000..3e54624
--- /dev/null
+++ b/src/jobs/jobs.rs
@@ -0,0 +1,28 @@
+use sqlx::sqlite::SqlitePool;
+use tera::Tera;
+use tokio::time::{delay_for, Duration};
+
+use crate::db;
+use crate::jobs::weekly_report;
+use crate::model::config::Config;
+use crate::model::job::Job;
+
+pub async fn start(config: Config, pool: SqlitePool, templates: Tera) -> () {
+ loop {
+ if db::jobs::should_run(&pool, Job::WeeklyReport).await {
+ info!("Starting weekly report job");
+ if weekly_report::send(&config, &pool, &templates).await {
+ db::jobs::actualize_last_execution(&pool, Job::WeeklyReport)
+ .await;
+ }
+ }
+ if db::jobs::should_run(&pool, Job::MonthlyPayment).await {
+ info!("Starting monthly payment job");
+ db::payments::create_monthly_payments(&pool).await;
+ db::jobs::actualize_last_execution(&pool, Job::MonthlyPayment)
+ .await;
+ }
+ // Sleeping 8 hours
+ delay_for(Duration::from_secs(8 * 60 * 60)).await;
+ }
+}
diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs
new file mode 100644
index 0000000..be2ddac
--- /dev/null
+++ b/src/jobs/mod.rs
@@ -0,0 +1,2 @@
+pub mod jobs;
+mod weekly_report;
diff --git a/src/jobs/weekly_report.rs b/src/jobs/weekly_report.rs
new file mode 100644
index 0000000..819d30b
--- /dev/null
+++ b/src/jobs/weekly_report.rs
@@ -0,0 +1,55 @@
+use sqlx::sqlite::SqlitePool;
+use std::collections::HashMap;
+use tera::{Context, Tera};
+
+use crate::db;
+use crate::mail;
+use crate::model::config::Config;
+use crate::payer;
+
+pub async fn send(
+ config: &Config,
+ pool: &SqlitePool,
+ templates: &Tera,
+) -> bool {
+ match get_weekly_report(pool, templates).await {
+ Ok(report) => {
+ let users = db::users::list(pool).await;
+ mail::send(
+ config,
+ users.into_iter().map(|u| (u.email, u.name)).collect(),
+ "Budget — rapport hebdomadaire".to_string(),
+ report,
+ )
+ }
+ Err(err) => {
+ error!("Error preparing weekly report from template: {:?}", err);
+ false
+ }
+ }
+}
+
+async fn get_weekly_report(
+ pool: &SqlitePool,
+ templates: &Tera,
+) -> Result<String, tera::Error> {
+ let users = db::users::list(pool).await;
+ let incomes_from = db::incomes::defined_for_all(pool).await;
+ let user_incomes = match incomes_from {
+ Some(from) => db::incomes::cumulative(pool, from).await,
+ None => HashMap::new(),
+ };
+ let user_payments = db::payments::repartition(pool).await;
+ let exceeding_payers =
+ payer::exceeding(&users, &user_incomes, &user_payments);
+
+ let last_week_payments = db::payments::last_week(pool).await;
+ let last_week_incomes = db::incomes::last_week(pool).await;
+
+ let mut context = Context::new();
+ context.insert("exceeding_payers", &exceeding_payers);
+ context.insert("payments", &last_week_payments);
+ context.insert("incomes", &last_week_incomes);
+
+ templates.render("report/report.j2", &context)
+}
diff --git a/src/mail.rs b/src/mail.rs
new file mode 100644
index 0000000..d86cff3
--- /dev/null
+++ b/src/mail.rs
@@ -0,0 +1,59 @@
+use lettre::sendmail::SendmailTransport;
+use lettre::{SendableEmail, Transport};
+use lettre_email::Email;
+
+use crate::model::config::Config;
+
+static FROM: &str = "contact@guyonvarch.me";
+
+pub fn send(
+ config: &Config,
+ to: Vec<(String, String)>,
+ subject: String,
+ message: String,
+) -> bool {
+ match prepare_email(to.clone(), subject.clone(), message.clone()) {
+ Ok(email) => {
+ if config.mock_mails {
+ let formatted_to = to
+ .into_iter()
+ .map(|t| t.0)
+ .collect::<Vec<String>>()
+ .join(", ");
+ info!(
+ "MOCK MAIL\nto: {}\nsubject: {}\n\n{}",
+ formatted_to, subject, message
+ );
+ true
+ } else {
+ let mut sender =
+ SendmailTransport::new_with_command(&config.sendmail_path);
+ match sender.send(email) {
+ Ok(_) => true,
+ Err(err) => {
+ error!("Error sending email: {:?}", err);
+ false
+ }
+ }
+ }
+ }
+ Err(err) => {
+ error!("Error preparing email: {:?}", err);
+ false
+ }
+ }
+}
+
+fn prepare_email(
+ to: Vec<(String, String)>,
+ subject: String,
+ message: String,
+) -> Result<SendableEmail, lettre_email::error::Error> {
+ let mut email = Email::builder().from(FROM).subject(subject).text(message);
+
+ for (address, name) in to.iter() {
+ email = email.to((address, name));
+ }
+
+ email.build().map(|e| e.into())
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..502f608
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,88 @@
+use hyper::service::{make_service_fn, service_fn};
+use hyper::Server;
+use sqlx::sqlite::SqlitePool;
+use std::convert::Infallible;
+use std::net::SocketAddr;
+use structopt::StructOpt;
+
+#[macro_use]
+extern crate log;
+
+mod assets;
+mod controller;
+mod db;
+mod jobs;
+mod mail;
+mod model;
+mod payer;
+mod queries;
+mod routes;
+mod templates;
+mod utils;
+mod validation;
+
+use model::config::Config;
+
+#[derive(StructOpt)]
+#[structopt()]
+struct Opt {
+ #[structopt(short, long, default_value = "127.0.0.1:3000")]
+ address: SocketAddr,
+
+ #[structopt(short, long, default_value = "config.json")]
+ config: String,
+
+ #[structopt(short, long, default_value = "database.db")]
+ database: String,
+}
+
+#[tokio::main]
+async fn main() {
+ env_logger::Builder::from_env(
+ env_logger::Env::default().default_filter_or("warn"),
+ )
+ .init();
+
+ let opt = Opt::from_args();
+
+ let config_str = std::fs::read_to_string(&opt.config)
+ .expect(&format!("Missing {}", opt.config));
+ let config: Config = serde_json::from_str(&config_str).unwrap();
+
+ let pool = SqlitePool::connect(&format!("sqlite:{}", opt.database))
+ .await
+ .unwrap();
+
+ let assets = assets::get();
+
+ let templates = templates::get();
+
+ tokio::spawn(jobs::jobs::start(
+ config.clone(),
+ pool.clone(),
+ templates.clone(),
+ ));
+
+ let make_svc = make_service_fn(|_conn| {
+ let config = config.clone();
+ let pool = pool.clone();
+ let assets = assets.clone();
+ let templates = templates.clone();
+ async move {
+ Ok::<_, Infallible>(service_fn(move |req| {
+ routes::routes(
+ config.clone(),
+ pool.clone(),
+ assets.clone(),
+ templates.clone(),
+ req,
+ )
+ }))
+ }
+ });
+
+ info!("Starting server at {}", opt.address);
+ if let Err(e) = Server::bind(&opt.address).serve(make_svc).await {
+ error!("server error: {}", e);
+ }
+}
diff --git a/src/model/action.rs b/src/model/action.rs
new file mode 100644
index 0000000..a77543a
--- /dev/null
+++ b/src/model/action.rs
@@ -0,0 +1 @@
+use serde::Serialize;
diff --git a/src/model/category.rs b/src/model/category.rs
new file mode 100644
index 0000000..de08dea
--- /dev/null
+++ b/src/model/category.rs
@@ -0,0 +1,20 @@
+use serde::Serialize;
+
+#[derive(sqlx::FromRow, Serialize)]
+pub struct Category {
+ pub id: i64,
+ pub name: String,
+ pub color: String,
+}
+
+#[derive(Debug)]
+pub struct Create {
+ pub name: String,
+ pub color: String,
+}
+
+#[derive(Debug)]
+pub struct Update {
+ pub name: String,
+ pub color: String,
+}
diff --git a/src/model/config.rs b/src/model/config.rs
new file mode 100644
index 0000000..8d304e5
--- /dev/null
+++ b/src/model/config.rs
@@ -0,0 +1,8 @@
+use serde::Deserialize;
+
+#[derive(Clone, Deserialize)]
+pub struct Config {
+ pub secure_cookies: bool,
+ pub mock_mails: bool,
+ pub sendmail_path: String,
+}
diff --git a/src/model/frequency.rs b/src/model/frequency.rs
new file mode 100644
index 0000000..bb83e27
--- /dev/null
+++ b/src/model/frequency.rs
@@ -0,0 +1,31 @@
+use serde::{Deserialize, Serialize};
+use std::{fmt, str};
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, sqlx::Type)]
+pub enum Frequency {
+ Punctual,
+ Monthly,
+}
+
+impl fmt::Display for Frequency {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Frequency::Punctual => write!(f, "Punctual"),
+ Frequency::Monthly => write!(f, "Monthly"),
+ }
+ }
+}
+
+pub struct ParseFrequencyError;
+
+impl str::FromStr for Frequency {
+ type Err = ParseFrequencyError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "Punctual" => Ok(Frequency::Punctual),
+ "Monthly" => Ok(Frequency::Monthly),
+ _ => Err(ParseFrequencyError {}),
+ }
+ }
+}
diff --git a/src/model/income.rs b/src/model/income.rs
new file mode 100644
index 0000000..7bc888f
--- /dev/null
+++ b/src/model/income.rs
@@ -0,0 +1,40 @@
+use serde::Serialize;
+
+#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
+pub struct Stat {
+ pub date: String,
+ pub amount: i64,
+}
+
+#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
+pub struct Table {
+ pub id: i64,
+ pub date: String,
+ pub user: String,
+ pub amount: i64,
+}
+
+#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
+pub struct Form {
+ pub id: i64,
+ pub amount: i64,
+ pub user_id: i64,
+ pub month: i64,
+ pub year: i64,
+}
+
+#[derive(Debug)]
+pub struct Create {
+ pub user_id: i64,
+ pub amount: i64,
+ pub month: u32,
+ pub year: i32,
+}
+
+#[derive(Debug)]
+pub struct Update {
+ pub user_id: i64,
+ pub amount: i64,
+ pub month: u32,
+ pub year: i32,
+}
diff --git a/src/model/job.rs b/src/model/job.rs
new file mode 100644
index 0000000..74151ae
--- /dev/null
+++ b/src/model/job.rs
@@ -0,0 +1,5 @@
+#[derive(Debug, sqlx::Type)]
+pub enum Job {
+ MonthlyPayment,
+ WeeklyReport,
+}
diff --git a/src/model/login.rs b/src/model/login.rs
new file mode 100644
index 0000000..c7a10ba
--- /dev/null
+++ b/src/model/login.rs
@@ -0,0 +1,5 @@
+#[derive(Debug)]
+pub struct Login {
+ pub email: String,
+ pub password: String,
+}
diff --git a/src/model/mod.rs b/src/model/mod.rs
new file mode 100644
index 0000000..fb07721
--- /dev/null
+++ b/src/model/mod.rs
@@ -0,0 +1,9 @@
+pub mod category;
+pub mod config;
+pub mod frequency;
+pub mod income;
+pub mod job;
+pub mod login;
+pub mod payment;
+pub mod report;
+pub mod user;
diff --git a/src/model/payment.rs b/src/model/payment.rs
new file mode 100644
index 0000000..5ce6bb9
--- /dev/null
+++ b/src/model/payment.rs
@@ -0,0 +1,53 @@
+use chrono::NaiveDate;
+use serde::Serialize;
+
+use crate::model::frequency::Frequency;
+
+#[derive(Debug, sqlx::FromRow, Serialize)]
+pub struct Table {
+ pub id: i64,
+ pub name: String,
+ pub cost: i64,
+ pub user: String,
+ pub category_name: String,
+ pub category_color: String,
+ pub date: String,
+ pub frequency: Frequency,
+}
+
+#[derive(Debug, sqlx::FromRow, Serialize)]
+pub struct Form {
+ pub id: i64,
+ pub name: String,
+ pub cost: i64,
+ pub user_id: i64,
+ pub category_id: i64,
+ pub date: String,
+ pub frequency: Frequency,
+}
+
+#[derive(Debug, sqlx::FromRow, Serialize)]
+pub struct Stat {
+ pub start_date: String,
+ pub cost: i64,
+ pub category_id: i64,
+}
+
+#[derive(Debug)]
+pub struct Create {
+ pub name: String,
+ pub cost: i64,
+ pub user_id: i64,
+ pub category_id: i64,
+ pub date: NaiveDate,
+ pub frequency: Frequency,
+}
+
+#[derive(Debug)]
+pub struct Update {
+ pub name: String,
+ pub cost: i64,
+ pub user_id: i64,
+ pub category_id: i64,
+ pub date: NaiveDate,
+}
diff --git a/src/model/report.rs b/src/model/report.rs
new file mode 100644
index 0000000..4858402
--- /dev/null
+++ b/src/model/report.rs
@@ -0,0 +1,16 @@
+use serde::Serialize;
+
+#[derive(Debug, sqlx::FromRow, Serialize)]
+pub struct Report {
+ pub date: String,
+ pub name: String,
+ pub amount: i64,
+ pub action: Action,
+}
+
+#[derive(Debug, PartialEq, Serialize, sqlx::Type)]
+pub enum Action {
+ Created,
+ Updated,
+ Deleted,
+}
diff --git a/src/model/user.rs b/src/model/user.rs
new file mode 100644
index 0000000..e8a61bf
--- /dev/null
+++ b/src/model/user.rs
@@ -0,0 +1,8 @@
+use serde::Serialize;
+
+#[derive(Debug, sqlx::FromRow, Clone, Serialize)]
+pub struct User {
+ pub id: i64,
+ pub name: String,
+ pub email: String,
+}
diff --git a/src/payer.rs b/src/payer.rs
new file mode 100644
index 0000000..48cee52
--- /dev/null
+++ b/src/payer.rs
@@ -0,0 +1,38 @@
+use std::collections::HashMap;
+
+use crate::model::user::User;
+
+pub fn exceeding(
+ users: &Vec<User>,
+ user_incomes: &HashMap<i64, i64>,
+ user_payments: &HashMap<i64, i64>,
+) -> Vec<(String, i64)> {
+ let ratios = users.into_iter().map(|u| {
+ let income = *user_incomes.get(&u.id).unwrap_or(&0);
+ if income == 0 {
+ (u.name.clone(), 0, 0.0)
+ } else {
+ let payments = *user_payments.get(&u.id).unwrap_or(&0);
+ let ratio = payments as f64 / income as f64;
+ (u.name.clone(), income, ratio)
+ }
+ });
+ let min_ratio = ratios
+ .clone()
+ .map(|r| r.2)
+ .min_by(|r1, r2| {
+ ((r1 * 100_000_000.0).round() as i64)
+ .cmp(&((r2 * 100_000_000.0).round() as i64))
+ })
+ .unwrap_or(0.0);
+ ratios
+ .filter_map(|r| {
+ let exceeding = ((r.2 - min_ratio) * r.1 as f64).round() as i64;
+ if exceeding == 0 {
+ None
+ } else {
+ Some((r.0, exceeding))
+ }
+ })
+ .collect()
+}
diff --git a/src/queries.rs b/src/queries.rs
new file mode 100644
index 0000000..a7ba28c
--- /dev/null
+++ b/src/queries.rs
@@ -0,0 +1,62 @@
+use crate::model::frequency::Frequency;
+use serde::{Deserialize, Serialize};
+
+#[derive(Deserialize, Serialize, Clone)]
+pub struct Payments {
+ pub page: Option<i64>,
+ pub search: Option<String>,
+ pub frequency: Option<Frequency>,
+ pub highlight: Option<i64>,
+}
+
+pub fn payments_url(q: Payments) -> String {
+ let mut params = Vec::new();
+
+ match q.page {
+ None | Some(1) => (),
+ Some(p) => params.push(format!("page={}", p)),
+ };
+
+ match q.search {
+ Some(s) => {
+ if !s.is_empty() {
+ params.push(format!("search={}", s));
+ }
+ }
+ _ => (),
+ };
+
+ match q.frequency {
+ Some(Frequency::Monthly) => {
+ params.push("frequency=Monthly".to_string())
+ }
+ _ => (),
+ };
+
+ match q.highlight {
+ Some(id) => params.push(format!("highlight={}", id)),
+ _ => (),
+ };
+
+ if params.is_empty() {
+ "".to_string()
+ } else {
+ format!("?{}", params.join("&"))
+ }
+}
+
+#[derive(Deserialize, Serialize, Clone)]
+pub struct Incomes {
+ pub page: Option<i64>,
+ pub highlight: Option<i64>,
+}
+
+#[derive(Deserialize, Serialize, Clone)]
+pub struct Categories {
+ pub highlight: Option<i64>,
+}
+
+#[derive(Deserialize, Serialize)]
+pub struct PaymentCategory {
+ pub payment_name: String,
+}
diff --git a/src/routes.rs b/src/routes.rs
new file mode 100644
index 0000000..3d76ab1
--- /dev/null
+++ b/src/routes.rs
@@ -0,0 +1,219 @@
+use hyper::{Body, Method, Request, Response};
+use serde::Deserialize;
+use serde_urlencoded;
+use sqlx::sqlite::SqlitePool;
+use std::collections::HashMap;
+use std::convert::Infallible;
+use tera::Tera;
+use url::form_urlencoded;
+
+use crate::controller;
+use crate::controller::wallet::Wallet;
+use crate::db;
+use crate::model::config::Config;
+use crate::model::user::User;
+
+pub async fn routes(
+ config: Config,
+ pool: SqlitePool,
+ assets: HashMap<String, String>,
+ templates: Tera,
+ request: Request<Body>,
+) -> Result<Response<Body>, Infallible> {
+ let method = request.method();
+ let uri = request.uri();
+ let path = &uri.path().split('/').collect::<Vec<&str>>()[1..];
+
+ let response = match (method, path) {
+ (&Method::GET, ["login"]) => {
+ controller::login::page(&assets, &templates, None).await
+ }
+ (&Method::POST, ["login"]) => {
+ controller::login::login(
+ config,
+ &assets,
+ &templates,
+ body_form(request).await,
+ pool,
+ )
+ .await
+ }
+ (&Method::GET, ["assets", _, file]) => {
+ controller::utils::file(&format!("assets/{}", file)).await
+ }
+ _ => match connected_user(&pool, &request).await {
+ Some(user) => {
+ let wallet = Wallet {
+ pool,
+ assets,
+ templates,
+ user,
+ };
+ authenticated_routes(config, wallet, request).await
+ }
+ None => controller::utils::redirect("/login"),
+ },
+ };
+
+ Ok(response)
+}
+
+async fn connected_user(
+ pool: &SqlitePool,
+ request: &Request<Body>,
+) -> Option<User> {
+ let cookie = request.headers().get("COOKIE")?.to_str().ok()?;
+ let mut xs = cookie.split('=');
+ xs.next();
+ let login_token = xs.next()?;
+ db::users::get_by_login_token(&pool, login_token.to_string()).await
+}
+
+async fn authenticated_routes(
+ config: Config,
+ wallet: Wallet,
+ request: Request<Body>,
+) -> Response<Body> {
+ let method = request.method();
+ let uri = request.uri();
+ let path = &uri.path().split('/').collect::<Vec<&str>>()[1..];
+ let query = uri.query();
+
+ match (method, path) {
+ (&Method::GET, [""]) => {
+ controller::payments::table(&wallet, parse_query(query)).await
+ }
+ (&Method::GET, ["payment"]) => {
+ controller::payments::create_form(&wallet, parse_query(query)).await
+ }
+ (&Method::POST, ["payment", "create"]) => {
+ controller::payments::create(
+ &wallet,
+ parse_query(query),
+ body_form(request).await,
+ )
+ .await
+ }
+ (&Method::GET, ["payment", "category"]) => {
+ controller::payments::search_category(&wallet, parse_query(query))
+ .await
+ }
+ (&Method::GET, ["payment", id]) => {
+ controller::payments::update_form(
+ parse_id(id),
+ &wallet,
+ parse_query(query),
+ )
+ .await
+ }
+ (&Method::POST, ["payment", id, "update"]) => {
+ controller::payments::update(
+ parse_id(id),
+ &wallet,
+ parse_query(query),
+ body_form(request).await,
+ )
+ .await
+ }
+ (&Method::POST, ["payment", id, "delete"]) => {
+ controller::payments::delete(
+ parse_id(id),
+ &wallet,
+ parse_query(query),
+ )
+ .await
+ }
+ (&Method::GET, ["incomes"]) => {
+ controller::incomes::table(&wallet, parse_query(query)).await
+ }
+ (&Method::GET, ["income"]) => {
+ controller::incomes::create_form(&wallet, parse_query(query)).await
+ }
+ (&Method::POST, ["income", "create"]) => {
+ controller::incomes::create(
+ &wallet,
+ parse_query(query),
+ body_form(request).await,
+ )
+ .await
+ }
+ (&Method::GET, ["income", id]) => {
+ controller::incomes::update_form(
+ parse_id(id),
+ &wallet,
+ parse_query(query),
+ )
+ .await
+ }
+ (&Method::POST, ["income", id, "update"]) => {
+ controller::incomes::update(
+ parse_id(id),
+ &wallet,
+ parse_query(query),
+ body_form(request).await,
+ )
+ .await
+ }
+ (&Method::POST, ["income", id, "delete"]) => {
+ controller::incomes::delete(
+ parse_id(id),
+ &wallet,
+ parse_query(query),
+ )
+ .await
+ }
+ (&Method::GET, ["categories"]) => {
+ controller::categories::table(&wallet, parse_query(query)).await
+ }
+ (&Method::GET, ["category"]) => {
+ controller::categories::create_form(&wallet).await
+ }
+ (&Method::POST, ["category", "create"]) => {
+ controller::categories::create(&wallet, body_form(request).await)
+ .await
+ }
+ (&Method::GET, ["category", id]) => {
+ controller::categories::update_form(parse_id(id), &wallet).await
+ }
+ (&Method::POST, ["category", id, "update"]) => {
+ controller::categories::update(
+ parse_id(id),
+ &wallet,
+ body_form(request).await,
+ )
+ .await
+ }
+ (&Method::POST, ["category", id, "delete"]) => {
+ controller::categories::delete(parse_id(id), &wallet).await
+ }
+ (&Method::GET, ["balance"]) => controller::balance::get(&wallet).await,
+ (&Method::GET, ["statistics"]) => {
+ controller::statistics::get(&wallet).await
+ }
+ (&Method::POST, ["logout"]) => {
+ controller::login::logout(config, &wallet).await
+ }
+ _ => controller::error::error(
+ &wallet,
+ "Page introuvable",
+ "La page que recherchez n’existe pas.",
+ ),
+ }
+}
+
+fn parse_query<'a, T: Deserialize<'a>>(query: Option<&'a str>) -> T {
+ serde_urlencoded::from_str(query.unwrap_or("")).unwrap()
+}
+
+async fn body_form(request: Request<Body>) -> HashMap<String, String> {
+ match hyper::body::to_bytes(request).await {
+ Ok(bytes) => form_urlencoded::parse(bytes.as_ref())
+ .into_owned()
+ .collect::<HashMap<String, String>>(),
+ Err(_) => HashMap::new(),
+ }
+}
+
+fn parse_id(str: &str) -> i64 {
+ str.parse::<i64>().unwrap()
+}
diff --git a/src/templates.rs b/src/templates.rs
new file mode 100644
index 0000000..7e3753a
--- /dev/null
+++ b/src/templates.rs
@@ -0,0 +1,97 @@
+use serde::Serialize;
+use serde_json::json;
+use serde_json::value::Value;
+use std::collections::HashMap;
+use tera::Tera;
+use tera::{Error, Result};
+
+use crate::queries;
+
+#[derive(Debug, Serialize)]
+pub enum Header {
+ Payments,
+ Categories,
+ Incomes,
+ Balance,
+ Statistics,
+}
+
+pub fn get() -> Tera {
+ let mut tera = match Tera::new("templates/**/*") {
+ Ok(t) => t,
+ Err(e) => {
+ error!("Parsing error(s): {}", e);
+ ::std::process::exit(1);
+ }
+ };
+ tera.register_function("payments_params", payments_params);
+ tera.register_filter("numeric", numeric);
+ tera.register_filter("euros", euros);
+ tera
+}
+
+fn payments_params(args: &HashMap<String, Value>) -> Result<Value> {
+ let q = json!({
+ "page": args.get("page"),
+ "search": args.get("search"),
+ "frequency": args.get("frequency"),
+ "highlight": args.get("highlight"),
+ });
+
+ match serde_json::from_value(q) {
+ Ok(q) => Ok(json!(queries::payments_url(q))),
+ Err(msg) => Err(Error::msg(msg)),
+ }
+}
+
+fn euros(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
+ match value {
+ Value::Number(n) => {
+ if let Some(n) = n.as_i64() {
+ let str = rgrouped(n.abs().to_string(), 3).join(" ");
+ let sign = if n < 0 { "-" } else { "" };
+ Ok(json!(format!("{}{} €", sign, str)))
+ } else if let Some(n) = n.as_f64() {
+ Ok(json!(format!("{} €", n.to_string())))
+ } else {
+ Err(Error::msg("Error parsing number"))
+ }
+ }
+ _ => Err(Error::msg(format!("{:?} should be a number", value))),
+ }
+}
+
+fn numeric(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
+ match value {
+ Value::Number(n) => {
+ if let Some(n) = n.as_i64() {
+ let str = rgrouped(n.abs().to_string(), 3).join(" ");
+ let sign = if n < 0 { "-" } else { "" };
+ Ok(json!(format!("{}{}", sign, str)))
+ } else if let Some(n) = n.as_f64() {
+ Ok(json!(format!("{}", n.to_string())))
+ } else {
+ Err(Error::msg("Error parsing number"))
+ }
+ }
+ _ => Err(Error::msg(format!("{:?} should be a number", value))),
+ }
+}
+
+fn rgrouped(str: String, n: usize) -> Vec<String> {
+ let mut str = str.clone();
+ let mut l = str.len();
+ let mut res = vec![];
+ while l > n {
+ let str2 = str.clone();
+ let (start, end) = str2.split_at(l - n);
+ l -= n;
+ str = start.to_string();
+ res.push(end.to_string());
+ }
+ if !str.is_empty() {
+ res.push(str);
+ }
+ res.reverse();
+ res
+}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
new file mode 100644
index 0000000..481c63a
--- /dev/null
+++ b/src/utils/mod.rs
@@ -0,0 +1 @@
+pub mod text;
diff --git a/src/utils/text.rs b/src/utils/text.rs
new file mode 100644
index 0000000..c07ccee
--- /dev/null
+++ b/src/utils/text.rs
@@ -0,0 +1,19 @@
+pub fn format_search(str: &String) -> String {
+ unaccent(&str.to_lowercase())
+}
+
+pub fn unaccent(str: &String) -> String {
+ str.chars().map(unaccent_char).collect()
+}
+
+pub fn unaccent_char(c: char) -> char {
+ match c {
+ 'à' | 'â' => 'a',
+ 'ç' => 'c',
+ 'è' | 'é' | 'ê' | 'ë' => 'e',
+ 'î' | 'ï' => 'i',
+ 'ô' => 'o',
+ 'ù' | 'û' | 'ü' => 'u',
+ _ => c,
+ }
+}
diff --git a/src/validation/category.rs b/src/validation/category.rs
new file mode 100644
index 0000000..7b1b5d5
--- /dev/null
+++ b/src/validation/category.rs
@@ -0,0 +1,18 @@
+use std::collections::HashMap;
+
+use crate::model::category::{Create, Update};
+use crate::validation::utils::*;
+
+pub fn create(form: &HashMap<String, String>) -> Option<Create> {
+ Some(Create {
+ name: non_empty(form, "name")?,
+ color: color(form, "color")?,
+ })
+}
+
+pub fn update(form: &HashMap<String, String>) -> Option<Update> {
+ Some(Update {
+ name: non_empty(form, "name")?,
+ color: color(form, "color")?,
+ })
+}
diff --git a/src/validation/income.rs b/src/validation/income.rs
new file mode 100644
index 0000000..972e42a
--- /dev/null
+++ b/src/validation/income.rs
@@ -0,0 +1,22 @@
+use std::collections::HashMap;
+
+use crate::model::income::{Create, Update};
+use crate::validation::utils::*;
+
+pub fn create(form: &HashMap<String, String>) -> Option<Create> {
+ Some(Create {
+ user_id: parse::<i64>(form, "user_id")?,
+ amount: parse::<i64>(form, "amount")?,
+ month: parse::<u32>(form, "month")?,
+ year: parse::<i32>(form, "year")?,
+ })
+}
+
+pub fn update(form: &HashMap<String, String>) -> Option<Update> {
+ Some(Update {
+ user_id: parse::<i64>(form, "user_id")?,
+ amount: parse::<i64>(form, "amount")?,
+ month: parse::<u32>(form, "month")?,
+ year: parse::<i32>(form, "year")?,
+ })
+}
diff --git a/src/validation/login.rs b/src/validation/login.rs
new file mode 100644
index 0000000..e40bb23
--- /dev/null
+++ b/src/validation/login.rs
@@ -0,0 +1,11 @@
+use std::collections::HashMap;
+
+use crate::model::login::Login;
+use crate::validation::utils::*;
+
+pub fn login(form: &HashMap<String, String>) -> Option<Login> {
+ Some(Login {
+ email: non_empty(form, "email")?,
+ password: non_empty(form, "password")?,
+ })
+}
diff --git a/src/validation/mod.rs b/src/validation/mod.rs
new file mode 100644
index 0000000..181abc7
--- /dev/null
+++ b/src/validation/mod.rs
@@ -0,0 +1,5 @@
+pub mod category;
+pub mod income;
+pub mod login;
+pub mod payment;
+pub mod utils;
diff --git a/src/validation/payment.rs b/src/validation/payment.rs
new file mode 100644
index 0000000..36aa852
--- /dev/null
+++ b/src/validation/payment.rs
@@ -0,0 +1,25 @@
+use std::collections::HashMap;
+
+use crate::model::payment::{Create, Update};
+use crate::validation::utils::*;
+
+pub fn create(form: &HashMap<String, String>) -> Option<Create> {
+ Some(Create {
+ name: non_empty(form, "name")?,
+ cost: parse::<i64>(form, "cost")?,
+ user_id: parse::<i64>(form, "user_id")?,
+ category_id: parse::<i64>(form, "category_id")?,
+ date: date(form, "date")?,
+ frequency: frequency(form, "frequency")?,
+ })
+}
+
+pub fn update(form: &HashMap<String, String>) -> Option<Update> {
+ Some(Update {
+ name: non_empty(form, "name")?,
+ cost: parse::<i64>(form, "cost")?,
+ user_id: parse::<i64>(form, "user_id")?,
+ category_id: parse::<i64>(form, "category_id")?,
+ date: date(form, "date")?,
+ })
+}
diff --git a/src/validation/utils.rs b/src/validation/utils.rs
new file mode 100644
index 0000000..4bff40a
--- /dev/null
+++ b/src/validation/utils.rs
@@ -0,0 +1,54 @@
+use chrono::NaiveDate;
+use std::collections::HashMap;
+use std::str::FromStr;
+
+use crate::model::frequency::Frequency;
+
+pub fn non_empty(
+ form: &HashMap<String, String>,
+ field: &str,
+) -> Option<String> {
+ let s = form.get(field)?.trim();
+ if s.is_empty() {
+ None
+ } else {
+ Some(s.to_string())
+ }
+}
+
+pub fn parse<T: FromStr>(
+ form: &HashMap<String, String>,
+ field: &str,
+) -> Option<T> {
+ let s = form.get(field)?;
+ s.parse::<T>().ok()
+}
+
+pub fn date(form: &HashMap<String, String>, field: &str) -> Option<NaiveDate> {
+ let s = form.get(field)?;
+ NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()
+}
+
+pub fn frequency(
+ form: &HashMap<String, String>,
+ field: &str,
+) -> Option<Frequency> {
+ let s = form.get(field)?;
+ Frequency::from_str(s).ok()
+}
+
+pub fn color(form: &HashMap<String, String>, field: &str) -> Option<String> {
+ let s = form.get(field)?;
+ if s.len() == 7
+ && &s[0..1] == "#"
+ && s[1..]
+ .to_string()
+ .into_bytes()
+ .into_iter()
+ .all(|c| c.is_ascii_hexdigit())
+ {
+ Some(s.to_string())
+ } else {
+ None
+ }
+}
diff --git a/templates/balance.html b/templates/balance.html
new file mode 100644
index 0000000..15da854
--- /dev/null
+++ b/templates/balance.html
@@ -0,0 +1,107 @@
+{% extends "base.html" %}
+
+{% block title %}
+ Équilibre
+{% endblock title %}
+
+{% block main %}
+
+ <div>
+ {% if exceeding_payers %}
+ <ul class="g-Balance__ExceedingPayers">
+ {% for exceeding_payer in exceeding_payers %}
+ <li class="g-Balance__ExceedingPayer">
+ {{ exceeding_payer.0 }} : +{{ exceeding_payer.1 | euros() }}
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p class="g-Paragraph">
+ Les paiements sont équilibrés.
+ </p>
+ {% endif %}
+
+ {% if incomes_from %}
+ <h1 class="g-Title">
+ Revenus
+ </h1>
+
+ <div class="g-Table">
+ <div class="g-Table__Row g-Table__Row--Header">
+ <span class="g-Table__Cell"></span>
+ <span class="g-Table__Cell">Montant</span>
+ <span class="g-Table__Cell">Part</span>
+ </div>
+ {% for user_income in user_incomes %}
+ <div class="g-Table__Row">
+ <span class="g-Table__Cell">
+ {{ user_income.0 }}
+ </span>
+ <span class="g-Table__Cell g-Table__NumericCell">
+ {{ user_income.1 | euros() }}
+ </span>
+ <span class="g-Table__Cell g-Table__NumericCell">
+ {% if total_income > 0 %}
+ {{ user_income.1 / total_income * 100 | round() }} %
+ {% else %}
+ –
+ {% endif %}
+ </span>
+ </div>
+ {% endfor %}
+ <div class="g-Table__Row">
+ <span class="g-Table__Cell">
+ Total
+ </span>
+ <span class="g-Table__Cell g-Table__NumericCell">
+ {{ total_income | euros() }}
+ </span>
+ <span class="g-Table__Cell g-Table__NumericCell">
+ 100 %
+ </span>
+ </div>
+ </div>
+ {% endif %}
+
+ <h1 class="g-Title">
+ Paiements
+ </h1>
+
+ <div class="g-Table">
+ <div class="g-Table__Row g-Table__Row--Header">
+ <span class="g-Table__Cell"></span>
+ <span class="g-Table__Cell">Montant</span>
+ <span class="g-Table__Cell">Part</span>
+ </div>
+ {% for user_payment in user_payments %}
+ <div class="g-Table__Row">
+ <span class="g-Table__Cell">
+ {{ user_payment.0 }}
+ </span>
+ <span class="g-Table__Cell g-Table__NumericCell">
+ {{ user_payment.1 | euros() }}
+ </span>
+ <span class="g-Table__Cell g-Table__NumericCell">
+ {% if total_payments > 0 %}
+ {{ user_payment.1 / total_payments * 100 | round() }} %
+ {% else %}
+ –
+ {% endif %}
+ </span>
+ </div>
+ {% endfor %}
+ <div class="g-Table__Row">
+ <span class="g-Table__Cell">
+ Total
+ </span>
+ <span class="g-Table__Cell g-Table__NumericCell">
+ {{ total_payments | euros() }}
+ </span>
+ <span class="g-Table__Cell g-Table__NumericCell">
+ 100 %
+ </span>
+ </div>
+ </div>
+ </div>
+
+{% endblock main %}
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..f403d41
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html lang="fr">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Budget — {% block title %}{% endblock title %}</title>
+ <link rel="stylesheet" href="{{ assets | get(key="main.css") }}" />
+ <link rel="icon" href="{{ assets | get(key="icon.png") }}">
+ </head>
+
+ <body>
+
+ <header class="g-Header">
+
+ <div class="g-Header__Primary">
+ <div class="g-Header__Title">Budget</div>
+ {% if connected_user %}
+ <form action="/logout" method="POST">
+ {{ connected_user.name }}
+ <input class="g-Header__Logout" type="submit" value="(Déconnexion)" />
+ </form>
+ {% endif %}
+ </div>
+
+ {% if connected_user %}
+ <div class="g-Header__Secondary">
+
+ <a
+ href="/"
+ class="
+ g-Header__Link
+ {% if header == "Payments" %} g-Header__Link--Current {% endif %}
+ "
+ >
+ Paiements
+ </a>
+
+ <a
+ href="/incomes"
+ class="
+ g-Header__Link
+ {% if header == "Incomes" %} g-Header__Link--Current {% endif %}
+ "
+ >
+ Revenus
+ </a>
+
+ <a
+ href="/categories"
+ class="
+ g-Header__Link
+ {% if header == "Categories" %} g-Header__Link--Current {% endif %}
+ "
+ >
+ Catégories
+ </a>
+
+ <a
+ href="/balance"
+ class="
+ g-Header__Link
+ {% if header == "Balance" %} g-Header__Link--Current {% endif %}
+ "
+ >
+ Équilibre
+ </a>
+
+ <a
+ href="/statistics"
+ class="
+ g-Header__Link
+ {% if header == "Statistics" %} g-Header__Link--Current {% endif %}
+ "
+ >
+ Statistiques
+ </a>
+
+ </div>
+ {% endif %}
+
+ </header>
+
+ <main class="g-Main">
+ {% block main %}{% endblock main %}
+ </main>
+
+ <script src="{{ assets | get(key="main.js") }}">
+ </script>
+
+ </body>
+</html>
diff --git a/templates/category/create.html b/templates/category/create.html
new file mode 100644
index 0000000..e206898
--- /dev/null
+++ b/templates/category/create.html
@@ -0,0 +1,51 @@
+{% extends "base.html" %}
+
+{% block title %}
+ Nouvelle catégorie
+{% endblock title %}
+
+{% block main %}
+
+ <div>
+ <p class="g-Paragraph">
+ <a class="g-Link g-Media__Large" href="/categories">
+ Retour aux categories
+ </a>
+ </p>
+
+ <form class="g-Form" action="/category/create" method="POST">
+ <h1 class="g-H1">
+ Nouvelle catégorie
+ </h1>
+
+ {% if error %}
+ <div class="g-Form__Error">{{ error }}</div>
+ {% endif %}
+
+ <label class="g-Form__Label" for="name">Nom</label>
+ <input
+ name="name"
+ class="g-Form__Input"
+ id="name"
+ value="{{ form.name | default(value="") }}"
+ required
+ {% if not form %} autofocus {% endif %}
+ />
+
+ <label class="g-Form__Label" for="color">Couleur</label>
+ <input
+ name="color"
+ type="color"
+ class="g-Form__Input g-Form__InputColor"
+ id="color"
+ value="{{ form.color | default(value="") }}"
+ required
+ />
+
+ <div>
+ <input class="g-Button__Validate" type="submit" value="Créer" />
+ </div>
+ </form>
+ </div>
+
+{% endblock main %}
diff --git a/templates/category/table.html b/templates/category/table.html
new file mode 100644
index 0000000..896304a
--- /dev/null
+++ b/templates/category/table.html
@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+
+{% block title %}
+ Catégories
+{% endblock title %}
+
+{% block main %}
+
+ <div class="g-Paragraph g-Payments__Header">
+ <a class="g-Button__Validate" href="/category">Nouveau</a>
+ </div>
+
+ {% if not categories %}
+
+ <div class="g-Payments__NoResults">
+ Il n’y a aucune catégorie.
+ </div>
+
+ {% else %}
+
+ <div class="g-Table">
+ {% for category in categories %}
+ <a
+ class="g-Table__Row {% if highlight == category.id %} g-Table__Row--Highlight {% endif %}"
+ href="/category/{{ category.id }}"
+ >
+ <span
+ class="g-Table__Cell"
+ style="color: {{ category.color }}"
+ >
+ {{ category.name }}
+ </span>
+ </a>
+ {% endfor %}
+ </div>
+
+ {% endif %}
+{% endblock main %}
diff --git a/templates/category/update.html b/templates/category/update.html
new file mode 100644
index 0000000..a4c1481
--- /dev/null
+++ b/templates/category/update.html
@@ -0,0 +1,85 @@
+{% extends "base.html" %}
+
+{% block title %}
+ Catégorie {{ id }}
+{% endblock title %}
+
+{% block main %}
+
+ <div>
+ <p class="g-Paragraph">
+ <a class="g-Link g-Media__Large" href="/categories">
+ Retour aux catégories
+ </a>
+ </p>
+
+ {% if error %}
+ <div class="g-Form__Error">{{ error }}</div>
+ {% endif %}
+
+ {% if not category %}
+
+ La catégorie n’a pas été trouvée.
+
+ {% else %}
+
+ <form
+ class="g-Form"
+ action="/category/{{ category.id }}/update"
+ method="POST"
+ >
+ <h1 class="g-H1">Modification</h1>
+
+ <label class="g-Form__Label" for="name">Nom</label>
+ <input
+ name="name"
+ class="g-Form__Input"
+ id="name"
+ value="{{ form.name | default(value=category.name) }}"
+ required
+ />
+
+ <label class="g-Form__Label" for="color">Couleur</label>
+ <input
+ name="color"
+ type="color"
+ class="g-Form__Input g-Form__InputColor"
+ id="color"
+ value="{{ form.color | default(value=category.color) }}"
+ required
+ />
+
+ <div>
+ <input class="g-Button__Validate" type="submit" value="Modifier" />
+ </div>
+ </form>
+
+ <form
+ class="g-Form"
+ action="/category/{{ category.id }}/delete"
+ method="POST"
+ >
+ <h1 class="g-H1">Suppression</h1>
+
+ {% if is_category_used %}
+ <p>
+ La catégorie ne peut pas être supprimée car elle est actuellement
+ utilisée.
+ </p>
+ {% else %}
+ <label class="g-Form__Label" for="remove-input">
+ Veuillez recopier le nom de la catégorie : « {{ category.name }} ».
+ </label>
+
+ <input name="remove-input" class="g-Form__Input" id="remove-input" data-name="{{ category.name }}" />
+
+ <div>
+ <input class="g-Button__Danger" type="submit" value="Supprimer" id="remove-button" disabled />
+ </div>
+ {% endif %}
+ </form>
+
+ {% endif %}
+ </div>
+
+{% endblock main %}
diff --git a/templates/error.html b/templates/error.html
new file mode 100644
index 0000000..b3049a9
--- /dev/null
+++ b/templates/error.html
@@ -0,0 +1,17 @@
+{% extends "base.html" %}
+
+{% block title %}
+ {{ title }}
+{% endblock title %}
+
+{% block main %}
+ <p class="g-Paragraph">
+ {{ message }}
+ </p>
+
+ <p class="g-Paragraph">
+ <a href="/" class="g-Link">
+ Retour à l’accueil
+ </a>
+ </p>
+{% endblock main %}
diff --git a/templates/income/create.html b/templates/income/create.html
new file mode 100644
index 0000000..b74dddd
--- /dev/null
+++ b/templates/income/create.html
@@ -0,0 +1,89 @@
+{% extends "base.html" %}
+
+{% block title %}
+ Nouveau revenu
+{% endblock title %}
+
+{% block main %}
+
+ <div>
+ <p class="g-Paragraph">
+ <a
+ class="g-Link g-Media__Large"
+ href="/incomes?page={{ query.page | default(value=1) }}"
+ >
+ Retour aux revenus
+ </a>
+ </p>
+
+ <form
+ class="g-Form"
+ action="/income/create?page={{ query.page | default(value=1) }}"
+ method="POST"
+ >
+ <h1 class="g-H1">
+ Nouveau revenu
+ </h1>
+
+ {% if error %}
+ <div class="g-Form__Error">{{ error }}</div>
+ {% endif %}
+
+ <label class="g-Form__Label" for="amount">Montant</label>
+ <input
+ name="amount"
+ type="number"
+ class="g-Form__Input"
+ id="amount"
+ value="{{ form.amount | default(value="") }}"
+ required
+ {% if not form %} autofocus {% endif %}
+ />
+
+ {% set user_id = form.user_id | default(value="" ~ connected_user.id) %}
+
+ <label class="g-Form__Label" for="user_id">Personne</label>
+ <select name="user_id" id="user_id" class="g-Form__Select" required>
+ {% for user in users %}
+ <option
+ value="{{ user.id }}"
+ {% if "" ~ user.id == user_id %} selected {% endif %}
+ >
+ {{ user.name }}
+ </option>
+ {% endfor %}
+ </select>
+
+ {% set month_index = form.month | default(value="" ~ current_month) %}
+
+ <label class="g-Form__Label" for="month">Mois</label>
+ <select name="month" id="month" class="g-Form__Select" required>
+ {% for month in months %}
+ <option
+ value="{{ loop.index }}"
+ {% if "" ~ loop.index == month_index %}
+ selected
+ {% endif %}
+ >
+ {{ month }}
+ </option>
+ {% endfor %}
+ </select>
+
+ <label class="g-Form__Label" for="year">Année</label>
+ <input
+ name="year"
+ type="number"
+ class="g-Form__Input"
+ id="year"
+ value="{{ form.year | default(value=now() | date(format="%Y")) }}"
+ required
+ />
+
+ <div>
+ <input class="g-Button__Validate" type="submit" value="Créer" />
+ </div>
+ </form>
+ </div>
+
+{% endblock main %}
diff --git a/templates/income/table.html b/templates/income/table.html
new file mode 100644
index 0000000..efd82a7
--- /dev/null
+++ b/templates/income/table.html
@@ -0,0 +1,55 @@
+{% import "macros/paging.html" as paging %}
+
+{% extends "base.html" %}
+
+{% block title %}
+ Revenus
+{% endblock title %}
+
+{% block main %}
+
+ <div class="g-Paragraph g-Payments__Header">
+ <a
+ class="g-Button__Validate"
+ href="/income?page={{ page | default(value=1) }}"
+ >
+ Nouveau
+ </a>
+ </div>
+
+ {% if not incomes %}
+
+ <div class="g-Payments__NoResults">
+ Il n’y a aucun revenu.
+ </div>
+
+ {% else %}
+
+ <div class="g-Table">
+ <div class="g-Table__Row g-Table__Row--Header">
+ <span class="g-Table__Cell">Montant</span>
+ <span class="g-Table__Cell">Personne</span>
+ <span class="g-Table__Cell">Mois</span>
+ </div>
+ {% for income in incomes %}
+ <a
+ class="g-Table__Row {% if highlight == income.id %} g-Table__Row--Highlight {% endif %}"
+ href="/income/{{ income.id }}?page={{ page | default(value=1) }}"
+ >
+ <span class="g-Table__Cell g-Table__NumericCell">
+ {{ income.amount | euros() }}
+ </span>
+ <span class="g-Table__Cell">{{ income.user }}</span>
+ <span class="g-Table__Cell">{{ income.date }}</span>
+ </a>
+ {% endfor %}
+ </div>
+
+ {{ paging::paging(
+ url="/incomes",
+ page=page,
+ max_page=max_page
+ ) }}
+
+ {% endif %}
+{% endblock main %}
diff --git a/templates/income/update.html b/templates/income/update.html
new file mode 100644
index 0000000..6dd649a
--- /dev/null
+++ b/templates/income/update.html
@@ -0,0 +1,117 @@
+{% extends "base.html" %}
+
+{% block title %}
+ Revenu {{ id }}
+{% endblock title %}
+
+{% block main %}
+
+ <div>
+ <p class="g-Paragraph">
+ <a
+ class="g-Link g-Media__Large"
+ href="/incomes?page={{ query.page | default(value=1) }}"
+ >
+ Retour aux revenus
+ </a>
+ </p>
+
+ {% if error %}
+ <div class="g-Form__Error">{{ error }}</div>
+ {% endif %}
+
+ {% if not income %}
+
+ Le revenu n’a pas été trouvé.
+
+ {% else %}
+
+ <form
+ class="g-Form"
+ action="/income/{{ income.id }}/update"
+ method="POST"
+ >
+ <h1 class="g-H1">Modification</h1>
+
+ <label class="g-Form__Label" for="amount">Montant</label>
+ <input
+ name="amount"
+ type="number"
+ class="g-Form__Input"
+ id="amount"
+ value="{{ form.amount | default(value=income.amount) }}"
+ required
+ />
+
+ {% set user_id = form.user_id | default(value="" ~ income.user_id) %}
+
+ <label class="g-Form__Label" for="user_id">Personne</label>
+ <select name="user_id" id="user_id" class="g-Form__Select" required>
+ {% for user in users %}
+ <option
+ value="{{ user.id }}"
+ {% if "" ~ user.id == user_id %} selected {% endif %}
+ >
+ {{ user.name }}
+ </option>
+ {% endfor %}
+ </select>
+
+ {% set month_index = form.month | default(value="" ~ income.month) %}
+
+ <label class="g-Form__Label" for="month">Mois</label>
+ <select name="month" id="month" class="g-Form__Select" required>
+ {% for month in months %}
+ <option
+ value="{{ loop.index }}"
+ {% if "" ~ loop.index == month_index %} selected {% endif %}
+ >
+ {{ month }}
+ </option>
+ {% endfor %}
+ </select>
+
+ <label class="g-Form__Label" for="year">Année</label>
+ <input
+ name="year"
+ type="number"
+ class="g-Form__Input"
+ id="year"
+ value="{{ form.year | default(value=income.year) }}"
+ required
+ />
+
+ <div>
+ <input class="g-Button__Validate" type="submit" value="Modifier" />
+ </div>
+ </form>
+
+ <form
+ class="g-Form"
+ action="/income/{{ income.id }}/delete"
+ method="POST"
+ >
+ <h1 class="g-H1">Suppression</h1>
+
+ {% if is_category_used %}
+ <p>
+ La catégorie ne peut pas être supprimée car elle est actuellement
+ utilisée.
+ </p>
+ {% else %}
+ <label class="g-Form__Label" for="remove-input">
+ Veuillez recopier le montant du revenu : « {{ income.amount }} ».
+ </label>
+
+ <input name="remove-input" class="g-Form__Input" id="remove-input" data-name="{{ income.amount }}" />
+
+ <div>
+ <input class="g-Button__Danger" type="submit" value="Supprimer" id="remove-button" disabled />
+ </div>
+ {% endif %}
+ </form>
+
+ {% endif %}
+ </div>
+
+{% endblock main %}
diff --git a/templates/login.html b/templates/login.html
new file mode 100644
index 0000000..fd4cfe9
--- /dev/null
+++ b/templates/login.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+
+{% block title %}
+ Connexion
+{% endblock title %}
+
+{% block main %}
+
+ <div>
+ <form class="g-Login g-Form" action="/login" method="POST">
+
+ {% if error %}
+ <div class="g-Form__Error">{{ error }}</div>
+ {% endif %}
+
+ <label class="g-Form__Label" for="email">Email</label>
+ <input name="email" class="g-Form__Input" id="email" required autofocus />
+
+ <label class="g-Form__Label" for="password">Mot de passe</label>
+ <input name="password" type="password" class="g-Form__Input" id="password" required />
+
+ <div>
+ <input class="g-Login__Button g-Button__Validate" type="submit" value="Connexion" />
+ </div>
+
+ </form>
+ </div>
+
+{% endblock main %}
diff --git a/templates/macros/paging.html b/templates/macros/paging.html
new file mode 100644
index 0000000..59ba617
--- /dev/null
+++ b/templates/macros/paging.html
@@ -0,0 +1,55 @@
+{% macro paging(url, page, max_page) %}
+ {% if url is containing("?") %}
+ {% set sign = "&" %}
+ {% else %}
+ {% set sign = "?" %}
+ {% endif %}
+
+ <div class="g-Paging">
+ {% if page > 1 %}
+ <a
+ class="g-Paging__Link g-Paging__Link--Active"
+ href="{{ url }}"
+ >
+ ❬❬
+ </a>
+ <a
+ class="g-Paging__Link g-Paging__Link--Active"
+ href="{{ url }}{{ sign }}page={{ page - 1 }}"
+ >
+ ❬
+ </a>
+ {% else %}
+ <span class="g-Paging__Link">
+ ❬❬
+ </span>
+ <span class="g-Paging__Link">
+ ❬
+ </span>
+ {% endif %}
+
+ {{ page }} / {{ max_page }}
+
+ {% if page < max_page %}
+ <a
+ class="g-Paging__Link g-Paging__Link--Active"
+ href="{{ url }}{{ sign }}page={{ page + 1 }}"
+ >
+ ❭
+ </a>
+ <a
+ class="g-Paging__Link g-Paging__Link--Active"
+ href="{{ url }}{{ sign }}page={{ max_page }}"
+ >
+ ❭❭
+ </a>
+ {% else %}
+ <span class="g-Paging__Link">
+ ❭
+ </span>
+ <span class="g-Paging__Link">
+ ❭❭
+ </span>
+ {% endif %}
+ </div>
+{% endmacro paging %}
diff --git a/templates/payment/create.html b/templates/payment/create.html
new file mode 100644
index 0000000..aea6fcd
--- /dev/null
+++ b/templates/payment/create.html
@@ -0,0 +1,120 @@
+{% extends "base.html" %}
+
+{% block title %}
+ Nouveau paiement
+{% endblock title %}
+
+{% block main %}
+
+ <div>
+ <p class="g-Paragraph">
+ <a
+ class="g-Link g-Media__Large"
+ href="/{{ payments_params(
+ page=query.page,
+ search=query.search,
+ frequency=query.frequency
+ ) }}"
+ >
+ Retour aux paiements
+ </a>
+ </p>
+
+ <form class="g-Form" action="/payment/create" method="POST">
+ <h1 class="g-H1">
+ Nouveau paiement
+ {% if query.frequency != "Monthly" %}
+ ponctuel
+ {% else %}
+ mensuel
+ {% endif %}
+ </h1>
+
+ {% if error %}
+ <div class="g-Form__Error">{{ error }}</div>
+ {% endif %}
+
+ <label class="g-Form__Label" for="name">Nom</label>
+ <input
+ name="name"
+ class="g-Form__Input"
+ id="name"
+ value="{{ form.name | default(value="") }}"
+ required
+ {% if not form %} autofocus {% endif %}
+ />
+
+ <label class="g-Form__Label" for="cost">Coût</label>
+ <input
+ name="cost"
+ type="number"
+ class="g-Form__Input"
+ id="cost"
+ value="{{ form.cost | default(value="") }}"
+ required
+ />
+
+ {% set user_id = form.user_id | default(value="" ~ connected_user.id) %}
+
+ <label class="g-Form__Label" for="user_id">Personne</label>
+ <select name="user_id" id="user_id" class="g-Form__Select" required>
+ {% for user in users %}
+ <option
+ value="{{ user.id }}"
+ {% if "" ~ user.id == user_id %}
+ selected
+ {% endif %}
+ >
+ {{ user.name }}
+ </option>
+ {% endfor %}
+ </select>
+
+ {% set category_id = form.category_id | default(value="") %}
+
+ <label class="g-Form__Label" for="category_id">Catégorie</label>
+ <select name="category_id" id="category_id" class="g-Form__Select" required>
+ {% for category in categories %}
+ <option
+ value="{{ category.id }}"
+ style="color: {{ category.color }}"
+ {% if "" ~ category.id == category_id %} selected {% endif %}
+ >
+ {{ category.name }}
+ </option>
+ {% endfor %}
+ </select>
+
+ {% set date = form.date | default(value=now() | date(format="%Y-%m-%d")) %}
+
+ {% if query.frequency != "Monthly" %}
+ <label class="g-Form__Label" for="date">Date</label>
+ <input
+ name="date"
+ type="date"
+ class="g-Form__Input"
+ id="date"
+ value="{{ date }}"
+ required
+ />
+ {% else %}
+ <input
+ name="date"
+ type="hidden"
+ value="{{ date }}"
+ />
+ {% endif %}
+
+ <input
+ type="hidden"
+ name="frequency"
+ value="{{ query.frequency | default(value="Punctual") }}"
+ />
+
+ <div>
+ <input class="g-Button__Validate" type="submit" value="Créer" />
+ </div>
+ </form>
+ </div>
+
+{% endblock main %}
diff --git a/templates/payment/table.html b/templates/payment/table.html
new file mode 100644
index 0000000..19b56b4
--- /dev/null
+++ b/templates/payment/table.html
@@ -0,0 +1,128 @@
+{% import "macros/paging.html" as paging %}
+
+{% extends "base.html" %}
+
+{% block title %}
+ Paiements
+{% endblock title %}
+
+{% block main %}
+
+ <div class="g-Paragraph g-Payments__Header">
+ <div class="g-Payments__FrequenciesAndSearch">
+ <div class="g-Payments__Frequencies">
+ {% if query.frequency == "Monthly" %}
+ <a
+ class="g-Payments__Frequency g-Link"
+ href="/{{ payments_params(frequency="Punctual") }}"
+ >
+ Ponctuels
+ </a>
+ /
+ <span class="g-Payments__Frequency g-Payments__Frequency--Selected">
+ Mensuels
+ </span>
+ {% else %}
+ <span class="g-Payments__Frequency g-Payments__Frequency--Selected">
+ Ponctuels
+ </span>
+ /
+ <a
+ class="g-Payments__Frequency g-Link"
+ href="/{{ payments_params(frequency="Monthly") }}"
+ >
+ Mensuels
+ </a>
+ {% endif %}
+ </div>
+
+ {% if query.frequency != "Monthly" %}
+ <form action="/" method="GET" class="g-Payments__Search">
+ <input
+ type="search"
+ name="search"
+ class="g-Form__Input g-Payments__SearchInput"
+ value="{{ query.search }}"
+ />
+ <input type="submit" class="g-Button__Search" value="🔍">
+ </form>
+ {% endif %}
+ </div>
+
+ <a
+ class="g-Button__Validate g-Payments__New"
+ href="/payment{{ payments_params(
+ page=query.page,
+ search=query.search,
+ frequency=query.frequency
+ ) }}"
+ >
+ Nouveau
+ </a>
+ </div>
+
+ {% if not payments %}
+
+ <div class="g-Payments__NoResults">
+ Aucun paiement ne correspond à votre recherche.
+ </div>
+
+ {% else %}
+
+ <div class="g-Paragraph">
+ {{ count | numeric }} paiement{{ count | pluralize }} comptabilisant {{ total_cost | euros() }}.
+ </div>
+
+ <div class="g-Table">
+ <div class="g-Table__Row g-Table__Row--Header">
+ <span class="g-Table__Cell">Nom</span>
+ <span class="g-Table__Cell">Coût</span>
+ <span class="g-Table__Cell">Personne</span>
+ <span class="g-Media__Large g-Table__Cell">Catégorie</span>
+ {% if query.frequency != "Monthly" %}
+ <span class="g-Table__Cell">Date</span>
+ {% endif %}
+ </div>
+ {% for payment in payments %}
+ <a
+ class="g-Table__Row {% if query.highlight == payment.id %} g-Table__Row--Highlight {% endif %}"
+ href="/payment/{{ payment.id }}{{ payments_params(
+ page=query.page,
+ search=query.search,
+ frequency=query.frequency
+ ) }}"
+ >
+ <span class="g-Table__Cell">{{ payment.name }}</span>
+ <span class="
+ g-Table__Cell
+ g-Table__NumericCell
+ {% if payment.cost < 0 %} g-Payments__Refund {% endif %}
+ ">
+ {{ payment.cost | euros() }}
+ </span>
+ <span class="g-Table__Cell">{{ payment.user }}</span>
+ <span class="g-Table__Cell g-Media__Large">
+ <span style="color: {{ payment.category_color }}">
+ {{ payment.category_name }}
+ </span>
+ </span>
+ {% if query.frequency != "Monthly" %}
+ <span class="g-Table__Cell">
+ {{ payment.date }}
+ </span>
+ {% endif %}
+ </a>
+ {% endfor %}
+ </div>
+
+ {{ paging::paging(
+ url="/" ~ payments_params(
+ search=query.search,
+ frequency=query.frequency
+ ),
+ page=page,
+ max_page=max_page
+ ) }}
+
+ {% endif %}
+{% endblock main %}
diff --git a/templates/payment/update.html b/templates/payment/update.html
new file mode 100644
index 0000000..25e6915
--- /dev/null
+++ b/templates/payment/update.html
@@ -0,0 +1,144 @@
+{% extends "base.html" %}
+
+{% block title %}
+ Paiement {{ id }}
+{% endblock title %}
+
+{% block main %}
+
+ <div>
+ <p class="g-Paragraph">
+ <a
+ class="g-Link g-Media__Large"
+ href="/{{ payments_params(
+ page=query.page,
+ search=query.search,
+ frequency=query.frequency
+ ) }}"
+ >
+ Retour aux paiements
+ </a>
+ </p>
+
+ {% if error %}
+ <div class="g-Form__Error">{{ error }}</div>
+ {% endif %}
+
+ {% if not payment %}
+
+ Le paiement n’a pas été trouvé.
+
+ {% else %}
+
+ <form
+ class="g-Form"
+ action="/payment/{{ payment.id }}/update{{ payments_params(
+ page=query.page,
+ search=query.search,
+ frequency=query.frequency,
+ highlight=query.highlight
+ ) }}"
+ method="POST"
+ >
+ <h1 class="g-H1">Modification</h1>
+
+ <label class="g-Form__Label" for="name">Nom</label>
+ <input
+ name="name"
+ class="g-Form__Input"
+ id="name"
+ value="{{ form.name | default(value=payment.name) }}"
+ required
+ />
+
+ <label class="g-Form__Label" for="cost">Coût</label>
+ <input
+ name="cost"
+ type="number"
+ class="g-Form__Input"
+ id="cost"
+ value="{{ form.cost | default(value=payment.cost) }}"
+ required
+ />
+
+ {% set user_id = form.user_id | default(value="" ~ payment.user_id) %}
+
+ <label class="g-Form__Label" for="user_id">Personne</label>
+ <select name="user_id" id="user_id" class="g-Form__Select" required>
+ {% for user in users %}
+ <option
+ value="{{ user.id }}"
+ {% if "" ~ user.id == user_id %} selected {% endif %}
+ >
+ {{ user.name }}
+ </option>
+ {% endfor %}
+ </select>
+
+ {% set category_id = form.category_id | default(value="" ~ payment.category_id) %}
+
+ <label class="g-Form__Label" for="category_id">Catégorie</label>
+ <select name="category_id" id="category_id" class="g-Form__Select" required>
+ {% for category in categories %}
+ <option
+ value="{{ category.id }}"
+ style="color: {{ category.color }}"
+ {% if "" ~ category.id == category_id %} selected {% endif %}
+ >
+ {{ category.name }}
+ </option>
+ {% endfor %}
+ </select>
+
+ {% set date = form.date | default(value=payment.date) %}
+
+ {% if payment.frequency == "Punctual" %}
+ <label class="g-Form__Label" for="date">Date</label>
+ <input
+ name="date"
+ type="date"
+ class="g-Form__Input"
+ id="date"
+ value="{{ date }}"
+ required
+ />
+ {% else %}
+ <input
+ name="date"
+ type="hidden"
+ value="{{ date }}"
+ />
+ {% endif %}
+
+ <div>
+ <input class="g-Button__Validate" type="submit" value="Modifier" />
+ </div>
+ </form>
+
+ <form
+ class="g-Form"
+ action="/payment/{{ payment.id }}/delete{{ payments_params(
+ page=query.page,
+ search=query.search,
+ frequency=query.frequency,
+ highlight=query.highlight
+ ) }}"
+ method="POST"
+ >
+ <h1 class="g-H1">Suppression</h1>
+
+ <label class="g-Form__Label" for="remove-input">
+ Veuillez recopier le nom du paiement : « {{ payment.name }} ».
+ </label>
+
+ <input name="remove-input" class="g-Form__Input" id="remove-input" data-name="{{ payment.name }}" />
+
+ <div>
+ <input class="g-Button__Danger" type="submit" value="Supprimer" id="remove-button" disabled />
+ </div>
+ </form>
+
+ {% endif %}
+ </div>
+
+{% endblock main %}
diff --git a/templates/report/list.j2 b/templates/report/list.j2
new file mode 100644
index 0000000..ef53244
--- /dev/null
+++ b/templates/report/list.j2
@@ -0,0 +1,14 @@
+{% macro list(resource, action, xs) -%}
+
+{% if xs -%}
+
+ {% set s = xs | length | pluralize -%}
+
+ {{ xs | length }} {{ resource }}{{ s }} {{ action }}{{ s }} :
+
+ {% for x in xs -%}
+ - {{ x.date }} {{ x.name }} {{ x.amount | euros() }}
+ {% endfor %}
+{% endif -%}
+
+{% endmacro paging %}
diff --git a/templates/report/report.j2 b/templates/report/report.j2
new file mode 100644
index 0000000..d36f3ce
--- /dev/null
+++ b/templates/report/report.j2
@@ -0,0 +1,50 @@
+{% import "report/list.j2" as list %}
+
+{% if exceeding_payers -%}
+
+ Équilibre :
+
+ {% for exceeding_payer in exceeding_payers -%}
+ - {{ exceeding_payer.0 }} : +{{ exceeding_payer.1 | euros() }}
+ {% endfor %}
+{% else -%}
+
+ Les paiements sont équilibrés.
+
+{% endif %}{#
+
+#}{{ list::list(
+ resource="paiement",
+ action="créé",
+ xs=payments | filter(attribute="action", value="Created")
+) }}{#
+
+#}{{ list::list(
+ resource="paiement",
+ action="modifié",
+ xs=payments | filter(attribute="action", value="Updated")
+) }}{#
+
+#}{{ list::list(
+ resource="paiement",
+ action="supprimé",
+ xs=payments | filter(attribute="action", value="Deleted")
+) }}{#
+
+#}{{ list::list(
+ resource="revenu",
+ action="créé",
+ xs=incomes | filter(attribute="action", value="Created")
+) }}{#
+
+#}{{ list::list(
+ resource="revenu",
+ action="modifié",
+ xs=incomes | filter(attribute="action", value="Updated")
+) }}{#
+
+#}{{ list::list(
+ resource="revenu",
+ action="supprimé",
+ xs=incomes | filter(attribute="action", value="Deleted")
+) }}
diff --git a/templates/statistics.html b/templates/statistics.html
new file mode 100644
index 0000000..badbc6d
--- /dev/null
+++ b/templates/statistics.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+
+{% block title %}
+ Statistiques
+{% endblock title %}
+
+{% block main %}
+
+ <div class="g-Chart">
+ <canvas id="g-Chart__Canvas">
+ </canvas>
+ </div>
+
+ <div id="categories" hidden>
+ {{ json_categories }}
+ </div>
+
+ <div id="incomes" hidden>
+ {{ json_incomes }}
+ </div>
+
+ <div id="payments" hidden>
+ {{ json_payments }}
+ </div>
+
+ <script src="{{ assets | get(key="chart.js") }}">
+ </script>
+
+{% endblock main %}
diff --git a/validation/LICENSE b/validation/LICENSE
deleted file mode 100644
index 45644ff..0000000
--- a/validation/LICENSE
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- <one line to give the program's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- <program> Copyright (C) <year> <name of author>
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<http://www.gnu.org/licenses/>.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/validation/Setup.hs b/validation/Setup.hs
deleted file mode 100644
index 4467109..0000000
--- a/validation/Setup.hs
+++ /dev/null
@@ -1,2 +0,0 @@
-import Distribution.Simple
-main = defaultMain
diff --git a/validation/src/Data/Validation.hs b/validation/src/Data/Validation.hs
deleted file mode 100644
index e30202f..0000000
--- a/validation/src/Data/Validation.hs
+++ /dev/null
@@ -1,375 +0,0 @@
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE DeriveDataTypeable #-}
-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE TypeFamilies #-}
-
-#if __GLASGOW_HASKELL__ >= 702
-{-# LANGUAGE DeriveGeneric #-}
-#endif
-
--- | A data type similar to @Data.Either@ that accumulates failures.
-module Data.Validation
-(
- -- * Data type
- Validation(..)
- -- * Constructing validations
-, validate
-, validationNel
-, fromEither
-, liftError
- -- * Functions on validations
-, validation
-, toEither
-, orElse
-, valueOr
-, ensure
-, codiagonal
-, validationed
-, bindValidation
- -- * Prisms
- -- | These prisms are useful for writing code which is polymorphic in its
- -- choice of Either or Validation. This choice can then be made later by a
- -- user, depending on their needs.
- --
- -- An example of this style of usage can be found
- -- <https://github.com/qfpl/validation/blob/master/examples/src/PolymorphicEmail.hs here>
-, _Failure
-, _Success
- -- * Isomorphisms
-, Validate(..)
-, revalidate
-) where
-
-import Control.Applicative (Applicative (pure, (<*>)), (<$>))
-import Control.DeepSeq (NFData (rnf))
-import Control.Lens (over, under)
-import Control.Lens.Getter ((^.))
-import Control.Lens.Iso (Iso, Swapped (..), from, iso)
-import Control.Lens.Prism (Prism, prism)
-import Control.Lens.Review (( # ))
-import Data.Bifoldable (Bifoldable (bifoldr))
-import Data.Bifunctor (Bifunctor (bimap))
-import Data.Bitraversable (Bitraversable (bitraverse))
-import Data.Data (Data)
-import Data.Either (Either (Left, Right), either)
-import Data.Eq (Eq)
-import Data.Foldable (Foldable (foldr))
-import Data.Function (id, ($), (.))
-import Data.Functor (Functor (fmap))
-import Data.Functor.Alt (Alt ((<!>)))
-import Data.Functor.Apply (Apply ((<.>)))
-import Data.List.NonEmpty (NonEmpty)
-import Data.Monoid (Monoid (mappend, mempty))
-import Data.Ord (Ord)
-import Data.Semigroup (Semigroup ((<>)))
-import Data.Traversable (Traversable (traverse))
-import Data.Typeable (Typeable)
-#if __GLASGOW_HASKELL__ >= 702
-import GHC.Generics (Generic)
-#endif
-import Prelude (Maybe (..), Show)
-
-
--- | An @Validation@ is either a value of the type @err@ or @a@, similar to 'Either'. However,
--- the 'Applicative' instance for @Validation@ /accumulates/ errors using a 'Semigroup' on @err@.
--- In contrast, the @Applicative@ for @Either@ returns only the first error.
---
--- A consequence of this is that @Validation@ has no 'Data.Functor.Bind.Bind' or 'Control.Monad.Monad' instance. This is because
--- such an instance would violate the law that a Monad's 'Control.Monad.ap' must equal the
--- @Applicative@'s 'Control.Applicative.<*>'
---
--- An example of typical usage can be found <https://github.com/qfpl/validation/blob/master/examples/src/Email.hs here>.
---
-data Validation err a =
- Failure err
- | Success a
- deriving (
- Eq, Ord, Show, Data, Typeable
-#if __GLASGOW_HASKELL__ >= 702
- , Generic
-#endif
- )
-
-instance Functor (Validation err) where
- fmap _ (Failure e) =
- Failure e
- fmap f (Success a) =
- Success (f a)
- {-# INLINE fmap #-}
-
-instance Semigroup err => Apply (Validation err) where
- Failure e1 <.> b = Failure $ case b of
- Failure e2 -> e1 <> e2
- Success _ -> e1
- Success _ <.> Failure e2 =
- Failure e2
- Success f <.> Success a =
- Success (f a)
- {-# INLINE (<.>) #-}
-
-instance Semigroup err => Applicative (Validation err) where
- pure =
- Success
- (<*>) =
- (<.>)
-
--- | For two errors, this instance reports only the last of them.
-instance Alt (Validation err) where
- Failure _ <!> x =
- x
- Success a <!> _ =
- Success a
- {-# INLINE (<!>) #-}
-
-instance Foldable (Validation err) where
- foldr f x (Success a) =
- f a x
- foldr _ x (Failure _) =
- x
- {-# INLINE foldr #-}
-
-instance Traversable (Validation err) where
- traverse f (Success a) =
- Success <$> f a
- traverse _ (Failure e) =
- pure (Failure e)
- {-# INLINE traverse #-}
-
-instance Bifunctor Validation where
- bimap f _ (Failure e) =
- Failure (f e)
- bimap _ g (Success a) =
- Success (g a)
- {-# INLINE bimap #-}
-
-
-instance Bifoldable Validation where
- bifoldr _ g x (Success a) =
- g a x
- bifoldr f _ x (Failure e) =
- f e x
- {-# INLINE bifoldr #-}
-
-instance Bitraversable Validation where
- bitraverse _ g (Success a) =
- Success <$> g a
- bitraverse f _ (Failure e) =
- Failure <$> f e
- {-# INLINE bitraverse #-}
-
-appValidation ::
- (err -> err -> err)
- -> Validation err a
- -> Validation err a
- -> Validation err a
-appValidation m (Failure e1) (Failure e2) =
- Failure (e1 `m` e2)
-appValidation _ (Failure _) (Success a2) =
- Success a2
-appValidation _ (Success a1) (Failure _) =
- Success a1
-appValidation _ (Success a1) (Success _) =
- Success a1
-{-# INLINE appValidation #-}
-
-instance Semigroup e => Semigroup (Validation e a) where
- (<>) =
- appValidation (<>)
- {-# INLINE (<>) #-}
-
-instance Monoid e => Monoid (Validation e a) where
- mappend =
- appValidation mappend
- {-# INLINE mappend #-}
- mempty =
- Failure mempty
- {-# INLINE mempty #-}
-
-instance Swapped Validation where
- swapped =
- iso
- (\v -> case v of
- Failure e -> Success e
- Success a -> Failure a)
- (\v -> case v of
- Failure a -> Success a
- Success e -> Failure e)
- {-# INLINE swapped #-}
-
-instance (NFData e, NFData a) => NFData (Validation e a) where
- rnf v =
- case v of
- Failure e -> rnf e
- Success a -> rnf a
-
--- | 'validate's an @a@ producing an updated optional value, returning
--- @e@ in the empty case.
---
--- This can be thought of as having the less general type:
---
--- @
--- validate :: e -> (a -> Maybe b) -> a -> Validation e b
--- @
-validate :: Validate v => e -> (a -> Maybe b) -> a -> v e b
-validate e p a = case p a of
- Nothing -> _Failure # e
- Just b -> _Success # b
-
--- | 'validationNel' is 'liftError' specialised to 'NonEmpty' lists, since
--- they are a common semigroup to use.
-validationNel :: Either e a -> Validation (NonEmpty e) a
-validationNel = liftError pure
-
--- | Converts from 'Either' to 'Validation'.
-fromEither :: Either e a -> Validation e a
-fromEither = liftError id
-
--- | 'liftError' is useful for converting an 'Either' to an 'Validation'
--- when the @Left@ of the 'Either' needs to be lifted into a 'Semigroup'.
-liftError :: (b -> e) -> Either b a -> Validation e a
-liftError f = either (Failure . f) Success
-
--- | 'validation' is the catamorphism for @Validation@.
-validation :: (e -> c) -> (a -> c) -> Validation e a -> c
-validation ec ac v = case v of
- Failure e -> ec e
- Success a -> ac a
-
--- | Converts from 'Validation' to 'Either'.
-toEither :: Validation e a -> Either e a
-toEither = validation Left Right
-
--- | @v 'orElse' a@ returns @a@ when @v@ is Failure, and the @a@ in @Success a@.
---
--- This can be thought of as having the less general type:
---
--- @
--- orElse :: Validation e a -> a -> a
--- @
-orElse :: Validate v => v e a -> a -> a
-orElse v a = case v ^. _Validation of
- Failure _ -> a
- Success x -> x
-
--- | Return the @a@ or run the given function over the @e@.
---
--- This can be thought of as having the less general type:
---
--- @
--- valueOr :: (e -> a) -> Validation e a -> a
--- @
-valueOr :: Validate v => (e -> a) -> v e a -> a
-valueOr ea v = case v ^. _Validation of
- Failure e -> ea e
- Success a -> a
-
--- | 'codiagonal' gets the value out of either side.
-codiagonal :: Validation a a -> a
-codiagonal = valueOr id
-
--- | 'ensure' ensures that a validation remains unchanged upon failure,
--- updating a successful validation with an optional value that could fail
--- with @e@ otherwise.
---
--- This can be thought of as having the less general type:
---
--- @
--- ensure :: e -> (a -> Maybe b) -> Validation e a -> Validation e b
--- @
-ensure :: Validate v => e -> (a -> Maybe b) -> v e a -> v e b
-ensure e p =
- over _Validation $ \v -> case v of
- Failure x -> Failure x
- Success a -> validate e p a
-
--- | Run a function on anything with a Validate instance (usually Either)
--- as if it were a function on Validation
---
--- This can be thought of as having the type
---
--- @(Either e a -> Either e' a') -> Validation e a -> Validation e' a'@
-validationed :: Validate v => (v e a -> v e' a') -> Validation e a -> Validation e' a'
-validationed f = under _Validation f
-
--- | @bindValidation@ binds through an Validation, which is useful for
--- composing Validations sequentially. Note that despite having a bind
--- function of the correct type, Validation is not a monad.
--- The reason is, this bind does not accumulate errors, so it does not
--- agree with the Applicative instance.
---
--- There is nothing wrong with using this function, it just does not make a
--- valid @Monad@ instance.
-bindValidation :: Validation e a -> (a -> Validation e b) -> Validation e b
-bindValidation v f = case v of
- Failure e -> Failure e
- Success a -> f a
-
--- | The @Validate@ class carries around witnesses that the type @f@ is isomorphic
--- to Validation, and hence isomorphic to Either.
-class Validate f where
- _Validation ::
- Iso (f e a) (f g b) (Validation e a) (Validation g b)
-
- _Either ::
- Iso (f e a) (f g b) (Either e a) (Either g b)
- _Either =
- iso
- (\x -> case x ^. _Validation of
- Failure e -> Left e
- Success a -> Right a)
- (\x -> _Validation # case x of
- Left e -> Failure e
- Right a -> Success a)
- {-# INLINE _Either #-}
-
-instance Validate Validation where
- _Validation =
- id
- {-# INLINE _Validation #-}
- _Either =
- iso
- (\x -> case x of
- Failure e -> Left e
- Success a -> Right a)
- (\x -> case x of
- Left e -> Failure e
- Right a -> Success a)
- {-# INLINE _Either #-}
-
-instance Validate Either where
- _Validation =
- iso
- fromEither
- toEither
- {-# INLINE _Validation #-}
- _Either =
- id
- {-# INLINE _Either #-}
-
--- | This prism generalises 'Control.Lens.Prism._Left'. It targets the failure case of either 'Either' or 'Validation'.
-_Failure ::
- Validate f =>
- Prism (f e1 a) (f e2 a) e1 e2
-_Failure =
- prism
- (\x -> _Either # Left x)
- (\x -> case x ^. _Either of
- Left e -> Right e
- Right a -> Left (_Either # Right a))
-{-# INLINE _Failure #-}
-
--- | This prism generalises 'Control.Lens.Prism._Right'. It targets the success case of either 'Either' or 'Validation'.
-_Success ::
- Validate f =>
- Prism (f e a) (f e b) a b
-_Success =
- prism
- (\x -> _Either # Right x)
- (\x -> case x ^. _Either of
- Left e -> Left (_Either # Left e)
- Right a -> Right a)
-{-# INLINE _Success #-}
-
--- | 'revalidate' converts between any two instances of 'Validate'.
-revalidate :: (Validate f, Validate g) => Iso (f e1 s) (f e2 t) (g e1 s) (g e2 t)
-revalidate = _Validation . from _Validation
diff --git a/validation/validation.cabal b/validation/validation.cabal
deleted file mode 100644
index 60e5444..0000000
--- a/validation/validation.cabal
+++ /dev/null
@@ -1,23 +0,0 @@
-name: validation
-version: 1
-license: BSD3
-license-file: LICENSE
-author: Tony Morris <ʇǝu˙sıɹɹoɯʇ@ןןǝʞsɐɥ> <dibblego>, Nick Partridge <nkpart>
-maintainer: Tony Morris <ʇǝu˙sıɹɹoɯʇ@ןןǝʞsɐɥ> <dibblego>, Nick Partridge <nkpart>, Queensland Functional Programming Lab <oᴉ˙ldɟb@llǝʞsɐɥ>
-synopsis: A data-type like Either but with an accumulating Applicative
-category: Data
-cabal-version: >= 1.10
-build-type: Simple
-
-library
- Default-Language: Haskell2010
- Build-Depends:
- base >= 4.5 && < 5
- , deepseq >= 1.2 && < 1.5
- , semigroups >= 0.8 && < 1
- , semigroupoids >= 5 && < 6
- , bifunctors >= 5.1 && < 6
- , lens >= 4 && < 5
- Ghc-Options: -Wall
- Hs-Source-Dirs: src
- Exposed-Modules: Data.Validation