diff options
262 files changed, 8126 insertions, 12581 deletions
@@ -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 @@ -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 Binary files differindex 51ca51c..51ca51c 100644 --- a/public/images/icon.png +++ b/assets/icon.png 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() }) +} @@ -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 Binary files differnew file mode 100644 index 0000000..85bfc82 --- /dev/null +++ b/docs/balance.png diff --git a/docs/payments.png b/docs/payments.png Binary files differnew file mode 100644 index 0000000..d811cde --- /dev/null +++ b/docs/payments.png diff --git a/docs/statistics.png b/docs/statistics.png Binary files differnew file mode 100644 index 0000000..6284536 --- /dev/null +++ b/docs/statistics.png 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 |