Merge pull request 'Finished billing module' (#24) from billing_module into develop
Reviewed-on: #24
This commit is contained in:
commit
9bb55d7097
|
@ -56,6 +56,12 @@ dependencies = [
|
|||
"opaque-debug 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
|
@ -73,9 +79,9 @@ checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
|
|||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.5.2"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
|
@ -147,6 +153,17 @@ version = "0.13.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "bigdecimal"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1374191e2dd25f9ae02e3aa95041ed5d747fc77b3c102b49fe2dd9a8117a6244"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "binascii"
|
||||
version = "0.1.4"
|
||||
|
@ -161,9 +178,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
|||
|
||||
[[package]]
|
||||
name = "blake2b_simd"
|
||||
version = "0.5.11"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
|
||||
checksum = "72936ee4afc7f8f736d1c38383b56480b5497b4617b4a77bdbf1d2ababc76127"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
|
@ -250,8 +267,8 @@ checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"num-integer",
|
||||
"num-traits 0.2.14",
|
||||
"serde 1.0.125",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time 0.1.43",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
@ -289,15 +306,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b1b9d958c2b1368a663f05538fc1b5975adce1e19f435acceae987aceeeb369"
|
||||
checksum = "54ad70579325f1a38ea4c13412b82241c5900700a69785d73e2736bd65a33f86"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"json5",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"pathdiff",
|
||||
"ron",
|
||||
"rust-ini",
|
||||
"serde 1.0.125",
|
||||
"serde-hjson",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml",
|
||||
"yaml-rust",
|
||||
|
@ -348,6 +368,22 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.1.4"
|
||||
|
@ -440,14 +476,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "diesel"
|
||||
version = "1.4.6"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "047bfc4d5c3bd2ef6ca6f981941046113524b9a9f9a7cbdfdd7ff40f58e6f542"
|
||||
checksum = "b28135ecf6b7d446b43e27e225622a038cc4e2930a1022f51cdb97ada19b8e4d"
|
||||
dependencies = [
|
||||
"bigdecimal",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"diesel_derives",
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"pq-sys",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
|
@ -498,6 +538,15 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
|
||||
|
||||
[[package]]
|
||||
name = "dlv-list"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b"
|
||||
dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
|
@ -528,9 +577,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.8.3"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f"
|
||||
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime",
|
||||
|
@ -544,6 +593,7 @@ name = "errms"
|
|||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bigdecimal",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"config",
|
||||
|
@ -552,12 +602,13 @@ dependencies = [
|
|||
"email-address-parser",
|
||||
"env_logger",
|
||||
"iban_validate",
|
||||
"lettre",
|
||||
"log",
|
||||
"rand",
|
||||
"rocket",
|
||||
"rocket_dyn_templates",
|
||||
"rust-argon2",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
|
@ -569,6 +620,15 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "figment"
|
||||
version = "0.10.5"
|
||||
|
@ -577,7 +637,7 @@ checksum = "0ca029e813a72b7526d28273d25f3e4a2f365d1b7a1018a6f93ec9053a119763"
|
|||
dependencies = [
|
||||
"atomic",
|
||||
"pear",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
"toml",
|
||||
"uncased",
|
||||
"version_check",
|
||||
|
@ -601,6 +661,21 @@ 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 = "fsevent"
|
||||
version = "0.4.0"
|
||||
|
@ -818,7 +893,7 @@ dependencies = [
|
|||
"pest",
|
||||
"pest_derive",
|
||||
"quick-error",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
|
@ -827,6 +902,9 @@ name = "hashbrown"
|
|||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
|
@ -857,6 +935,17 @@ dependencies = [
|
|||
"digest 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"match_cfg",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.4"
|
||||
|
@ -865,7 +954,7 @@ checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
|
|||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
"itoa 0.4.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -912,7 +1001,7 @@ dependencies = [
|
|||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"itoa 0.4.7",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
|
@ -923,13 +1012,24 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "iban_validate"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae1dbb2046534ff3eda29fa47148f3e28011cbaf063dd5893edf89f47c3fe115"
|
||||
checksum = "cc1d358f7ae89819e8656f1b495c9d760a9ca315998b12d589dc516c9f81ed08"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||
dependencies = [
|
||||
"matches",
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.6.2"
|
||||
|
@ -938,7 +1038,7 @@ checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
|
|||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -991,6 +1091,23 @@ version = "0.4.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
|
||||
|
||||
[[package]]
|
||||
name = "json5"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
|
@ -1014,23 +1131,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "lexical-core"
|
||||
version = "0.7.6"
|
||||
name = "lettre"
|
||||
version = "0.10.0-rc.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
|
||||
checksum = "71d8da8f34d086b081c9cc3b57d3bb3b51d16fc06b5c848a188e2f14d58ac2a5"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hostname",
|
||||
"httpdate",
|
||||
"idna",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"nom",
|
||||
"once_cell",
|
||||
"quoted_printable",
|
||||
"regex",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.94"
|
||||
version = "0.2.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
|
||||
checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
|
@ -1065,7 +1193,7 @@ dependencies = [
|
|||
"cfg-if 1.0.0",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
|
@ -1075,6 +1203,18 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
||||
|
||||
[[package]]
|
||||
name = "match_cfg"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.0"
|
||||
|
@ -1087,6 +1227,12 @@ version = "0.3.16"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.6.23"
|
||||
|
@ -1173,6 +1319,24 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "net2"
|
||||
version = "0.2.37"
|
||||
|
@ -1186,12 +1350,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "5.1.2"
|
||||
version = "7.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
|
||||
checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109"
|
||||
dependencies = [
|
||||
"lexical-core",
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
|
@ -1231,6 +1395,17 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
|
@ -1238,16 +1413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits 0.2.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
|
||||
dependencies = [
|
||||
"num-traits 0.2.14",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1287,6 +1453,49 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-multimap"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485"
|
||||
dependencies = [
|
||||
"dlv-list",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.1"
|
||||
|
@ -1321,6 +1530,12 @@ dependencies = [
|
|||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||
|
||||
[[package]]
|
||||
name = "pear"
|
||||
version = "0.2.3"
|
||||
|
@ -1444,6 +1659,12 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.4.5"
|
||||
|
@ -1529,15 +1750,20 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.3"
|
||||
name = "quoted_printable"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
|
||||
checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_hc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1559,15 +1785,6 @@ dependencies = [
|
|||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.7"
|
||||
|
@ -1649,7 +1866,7 @@ dependencies = [
|
|||
"ref-cast",
|
||||
"rocket_codegen",
|
||||
"rocket_http",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"state",
|
||||
"tempfile",
|
||||
|
@ -1690,7 +1907,7 @@ dependencies = [
|
|||
"normpath",
|
||||
"notify",
|
||||
"rocket",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
|
@ -1713,7 +1930,7 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"ref-cast",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"stable-pattern",
|
||||
"state",
|
||||
|
@ -1724,10 +1941,21 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-argon2"
|
||||
version = "0.8.3"
|
||||
name = "ron"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
|
||||
checksum = "1b861ecaade43ac97886a512b360d01d66be9f41f3c61088b42cedf92e03d678"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bitflags",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-argon2"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b50162d19404029c1ceca6f6980fe40d45c8b369f6f44446fa14bb39573b5bb9"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blake2b_simd",
|
||||
|
@ -1737,9 +1965,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.13.0"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2"
|
||||
checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"ordered-multimap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
|
@ -1771,6 +2003,16 @@ 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 = "scoped-tls"
|
||||
version = "1.0.0"
|
||||
|
@ -1783,6 +2025,29 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "0.9.0"
|
||||
|
@ -1800,36 +2065,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "0.8.23"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
|
||||
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-hjson"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"num-traits 0.1.43",
|
||||
"regex",
|
||||
"serde 0.8.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.125"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
|
||||
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1838,13 +2085,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.64"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
|
||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"itoa 1.0.1",
|
||||
"ryu",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1948,12 +2195,6 @@ dependencies = [
|
|||
"loom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "stdweb"
|
||||
version = "0.4.20"
|
||||
|
@ -1976,7 +2217,7 @@ checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"syn",
|
||||
]
|
||||
|
@ -1990,7 +2231,7 @@ dependencies = [
|
|||
"base-x",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
|
@ -2091,6 +2332,21 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
|
||||
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 = "1.6.2"
|
||||
|
@ -2121,6 +2377,16 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.6"
|
||||
|
@ -2152,7 +2418,7 @@ version = "0.5.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
||||
dependencies = [
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2209,7 +2475,7 @@ version = "0.10.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42756bb9e708855de2f8a98195643dff31a97f0485d90d8467b39dc24be9e8fe"
|
||||
dependencies = [
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2224,7 +2490,7 @@ version = "0.9.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0"
|
||||
dependencies = [
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
|
@ -2234,6 +2500,21 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
|
@ -2257,7 +2538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde 1.0.125",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
24
Cargo.toml
24
Cargo.toml
|
@ -1,28 +1,28 @@
|
|||
[package]
|
||||
name = "errms"
|
||||
version = "0.2.0"
|
||||
authors = ["Keanu Doelle <ares@anghenfil.de>"]
|
||||
authors = ["Keanu Doelle <keanu@doelle.cc>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
config = "0.11.0"
|
||||
serde_derive = "1.0.125"
|
||||
serde = { version = "1.0.125", features = ["derive"] }
|
||||
serde_json = "1.0.64"
|
||||
config = "0.12.0"
|
||||
serde_derive = "1.0.136"
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = "1.0.79"
|
||||
log = "0.4.14"
|
||||
env_logger = "0.8.3"
|
||||
diesel = { version = "1.4.6", features = ["postgres", "uuidv07", "chrono", "serde_json"] } #uuidv07 vs uuid to use uuid >= 0.7
|
||||
env_logger = "0.9.0"
|
||||
diesel = { version = "1.4.8", features = ["postgres", "uuidv07", "chrono", "serde_json", "numeric"] } #uuidv07 vs uuid to use uuid >= 0.7
|
||||
diesel_geometry = "1.4.0"
|
||||
uuid = { version = "0.8.2", features = ["serde", "v4"] }
|
||||
rust-argon2 = "0.8.3"
|
||||
rust-argon2 = "1.0.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
chrono-tz = "0.6"
|
||||
rand = "0.8.3"
|
||||
iban_validate = "4.0.0"
|
||||
rand = "0.8.5"
|
||||
iban_validate = "4.0.1"
|
||||
base64 = "0.13.0"
|
||||
email-address-parser = "1.0.1"
|
||||
bigdecimal = "0.1.2"
|
||||
lettre = { version = "0.10.0-rc.4", features = ["tokio1", "tokio1-native-tls"] }
|
||||
|
||||
[dependencies.rocket]
|
||||
version = "0.5.0-rc.1"
|
||||
|
|
|
@ -23,8 +23,11 @@ default_timezone = "Etc/UTC"
|
|||
[mail]
|
||||
from = "No Reply <noreply@localhost>"
|
||||
reply_to = "support@localhost"
|
||||
#type_id for emails in communication_types table
|
||||
#type_id for emails in communication_types table to enable communicator email functionality
|
||||
communication_email_type_id = "a0a93b6e-f200-11ea-ad50-e86a6451da25"
|
||||
smtp_host = "mail.example.com"
|
||||
smtp_username = "user"
|
||||
smtp_password = "password"
|
||||
|
||||
[api]
|
||||
default_pagination_limit = 20
|
||||
|
@ -32,3 +35,5 @@ default_pagination_limit = 20
|
|||
[billing]
|
||||
#If set true members don't need billing related permissions if they are member_responsible for specific event
|
||||
member_responsible_overwrites_permissions = true
|
||||
send_personnel_billing_to_email = true
|
||||
personnel_billing_email = "receiver@localhost"
|
|
@ -20,15 +20,21 @@ user_support_email = "support@einsatz.online"
|
|||
new_member_default_role = "member"
|
||||
default_timezone = "Europe/Berlin"
|
||||
|
||||
|
||||
[mail]
|
||||
from = "No Reply <noreply@localhost>"
|
||||
reply_to = "support@localhost"
|
||||
#type_id for emails in communication_types table
|
||||
#type_id for emails in communication_types table to enable communicator email functionality
|
||||
communication_email_type_id = "a0a93b6e-f200-11ea-ad50-e86a6451da25"
|
||||
smtp_host = "mail.example.com"
|
||||
smtp_username = "user"
|
||||
smtp_password = "password"
|
||||
|
||||
[api]
|
||||
default_pagination_limit = 20
|
||||
|
||||
[billing]
|
||||
#If set true members don't need billing related permissions if they are member_responsible for specific event
|
||||
member_responsible_overwrites_permissions = true
|
||||
member_responsible_overwrites_permissions = true
|
||||
send_personnel_billing_to_email = true
|
||||
personnel_billing_email = "receiver@localhost"
|
|
@ -150,9 +150,9 @@ create table eu_instances
|
|||
template_id uuid not null
|
||||
constraint eu_instances_entities_entity_id_fk_2
|
||||
references entities
|
||||
on update cascade on delete cascade,
|
||||
name text not null,
|
||||
event_id uuid not null
|
||||
on update cascade,
|
||||
name text not null,
|
||||
event_id uuid not null
|
||||
constraint eu_instances_entities_entity_id_fk_3
|
||||
references entities
|
||||
on update cascade on delete cascade
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
-- Create new table billing states to define billing states for eu_instances
|
||||
create table billing_states
|
||||
(
|
||||
state_id text
|
||||
constraint billing_states_pk
|
||||
primary key,
|
||||
state_name text not null,
|
||||
description text,
|
||||
final_approve boolean default false not null
|
||||
final_approve boolean default false not null,
|
||||
"order" integer,
|
||||
entity_id uuid not null
|
||||
constraint billing_states_pk
|
||||
primary key
|
||||
constraint billing_states_entities_entity_id_fk
|
||||
references entities
|
||||
on update cascade
|
||||
);
|
||||
|
||||
create unique index billing_states_state_id_uindex
|
||||
on billing_states (state_name);
|
||||
|
||||
|
||||
-- Create new table personnel_billing_rates to define billing rates for personnel billing
|
||||
create table personnel_billing_rates
|
||||
(
|
||||
|
@ -38,11 +47,10 @@ alter table eu_instances
|
|||
add billing_rate_id uuid;
|
||||
|
||||
alter table eu_instances
|
||||
add billing_state_id text;
|
||||
add billing_state_id uuid;
|
||||
|
||||
alter table eu_instances
|
||||
add constraint eu_instances_billing_states_state_id_fk
|
||||
foreign key (billing_state_id) references billing_states;
|
||||
add constraint eu_instances_entities_entity_id_fk_4 foreign key (billing_state_id) references entities on update cascade;
|
||||
|
||||
alter table eu_instances
|
||||
add constraint eu_instances_personnel_billing_rates_billing_rate_id_fk
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
DELETE
|
||||
FROM roles_permissions
|
||||
WHERE permission_id = 'modules.event_billing.personnel.edit';
|
||||
|
||||
DELETE
|
||||
FROM roles_permissions
|
||||
WHERE permission_id = 'modules.event_billing.personnel.view';
|
||||
|
||||
DELETE
|
||||
FROM roles_permissions
|
||||
WHERE permission_id = 'modules.event_billing.approve';
|
||||
|
||||
DELETE
|
||||
FROM roles_permissions
|
||||
WHERE permission_id = 'modules.event_billing.view';
|
||||
|
||||
DELETE
|
||||
FROM roles_permissions
|
||||
WHERE permission_id = 'modules.event_billing.start_end_times.edit';
|
||||
|
||||
DELETE
|
||||
FROM roles_permissions
|
||||
WHERE permission_id = 'modules.event_billing.start_end_times.view';
|
|
@ -0,0 +1,18 @@
|
|||
-- Your SQL goes here
|
||||
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
|
||||
VALUES ('admin', 'modules.event_billing.approve', DEFAULT);
|
||||
|
||||
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
|
||||
VALUES ('admin', 'modules.event_billing.personnel.edit', DEFAULT);
|
||||
|
||||
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
|
||||
VALUES ('admin', 'modules.event_billing.personnel.view', DEFAULT);
|
||||
|
||||
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
|
||||
VALUES ('admin', 'modules.event_billing.start_end_times.edit', DEFAULT);
|
||||
|
||||
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
|
||||
VALUES ('admin', 'modules.event_billing.start_end_times.view', DEFAULT);
|
||||
|
||||
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
|
||||
VALUES ('admin', 'modules.event_billing.view', DEFAULT);
|
|
@ -0,0 +1,3 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
alter table billing_states
|
||||
drop column "order";
|
|
@ -0,0 +1,3 @@
|
|||
-- Your SQL goes here
|
||||
alter table billing_states
|
||||
add "order" int;
|
|
@ -0,0 +1,8 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
alter table eu_position_instances
|
||||
drop constraint eu_position_instances_entities_entity_id_fk_2;
|
||||
|
||||
alter table eu_position_instances
|
||||
add constraint eu_position_instances_entities_entity_id_fk_2
|
||||
foreign key (position_id) references entities
|
||||
on update cascade on delete restrict;
|
|
@ -0,0 +1,8 @@
|
|||
-- Your SQL goes here
|
||||
alter table eu_position_instances
|
||||
drop constraint eu_position_instances_entities_entity_id_fk_2;
|
||||
|
||||
alter table eu_position_instances
|
||||
add constraint eu_position_instances_entities_entity_id_fk_2
|
||||
foreign key (position_id) references entities
|
||||
on update cascade on delete restrict;
|
|
@ -0,0 +1,3 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
alter table eu_position_instances
|
||||
drop column billable;
|
|
@ -0,0 +1,3 @@
|
|||
-- Your SQL goes here
|
||||
alter table eu_position_instances
|
||||
add billable bool;
|
|
@ -0,0 +1,8 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
alter table eu_instances
|
||||
drop constraint eu_instances_entities_entity_id_fk_5;
|
||||
alter table eu_instances
|
||||
drop billing_state_author;
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
-- Your SQL goes here
|
||||
alter table eu_instances
|
||||
add billing_state_author uuid;
|
||||
|
||||
alter table eu_instances
|
||||
add constraint eu_instances_entities_entity_id_fk_5
|
||||
foreign key (billing_state_author) references entities
|
||||
on update cascade;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<a class="hide goto_edit_times" href="/portal/eb/edit_times?event_id={{event.entity_id}}">
|
||||
<button class="btn btn-primary">Abrechnung fortsetzen</button>
|
||||
</a>
|
||||
<a class="hide goto_personnel_billing" href="/portal/eb/personnel_billing?event_id={{event.entity_id}}">
|
||||
<button class="btn btn-primary">Abrechnung fortsetzen</button>
|
||||
</a>
|
||||
<a class="hide goto_close_event" href="/portal/eb/close_event?event_id={{event.entity_id}}">
|
||||
<button class="btn btn-primary" style="margin-right: 5px;">Abrechnung starten</button>
|
||||
</a>
|
||||
{{#each states}}
|
||||
<a class="hide goto_approve_{{entity_id}}"
|
||||
href="/portal/eb/approve?event_id={{../event.entity_id}}&stage={{entity_id}}">
|
||||
<button class="btn btn-success">{{description}}</button>
|
||||
</a>
|
||||
{{/each}}
|
|
@ -0,0 +1,30 @@
|
|||
<div class="instance" data-instance-id="{{instance_id}}">
|
||||
<div>
|
||||
<h3>{{name}}</h3>
|
||||
{{#each positions}}{{#if personnel_billing}}
|
||||
<div style="padding-left: 20px; padding-bottom: 10px;">
|
||||
<span><strong>{{taken_by_member}}</strong> als <em>{{position_name}}</em>, von {{real_time}} ≙ {{personnel_billing.fulfilled_time}} Stunden</span>
|
||||
<div style="padding-left: 20px;">
|
||||
<div class="row" style="border-bottom: 1px solid black">
|
||||
<span class="col-5">Pauschale</span>
|
||||
<span class="col-7" style="text-align: right">{{personnel_billing.money_from_lump_sum}} €</span>
|
||||
</div>
|
||||
<div class="row" style="border-bottom: 1px solid black">
|
||||
<span class="col-5">Stundengeld</span>
|
||||
<span class="col-7" style="text-align: right">{{personnel_billing.money_for_time}} €</span>
|
||||
</div>
|
||||
<div class="row" style="border-bottom: 1px solid black">
|
||||
<span class="col-5">Helfergeld</span>
|
||||
<span class="col-7" style="text-align: right;">{{personnel_billing.total_money}} €</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}{{/each}}
|
||||
<div style="padding-left: 40px;padding-bottom: 20px;">
|
||||
<div class="row" style="border-bottom: 3px double black">
|
||||
<span class="col-5">Gesamt</span>
|
||||
<span class="col-7" style="text-align: right; padding-left: 20px;">{{instance_total_sum}} €</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
<div style="padding-top:20px;">
|
||||
<div class="row" style="border-bottom: 5px double black">
|
||||
<h3 class="col-5">SUMME</h3>
|
||||
<b class="col-7" style="text-align: right; padding-left: 20px;">{{total_sum}} €</b>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,128 @@
|
|||
<div class="instance col-lg-4" data-instance-id="{{instance_id}}" data-template-id="{{template_id}}"
|
||||
style="padding: 2px;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{{name}}
|
||||
<button class="btn btn-success eb_eu_instance_save_btn" style="float: right;display:none;" type="button">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<p class="col-3">geplant:</p>
|
||||
<p class="col-9">
|
||||
{{planned_time}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">realer Beginn: </label>
|
||||
<div class="col-sm-9">
|
||||
<input class="form-control eb_eu_instance_real_start_time qsf" type="datetime-local"
|
||||
value="{{real_start_time}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">reales Ende: </label>
|
||||
<div class="col-sm-9">
|
||||
<input class="form-control eb_eu_instance_real_end_time qsf" type="datetime-local"
|
||||
value="{{real_end_time}}">
|
||||
</div>
|
||||
</div>
|
||||
<h5>Personal:</h5>
|
||||
<div class="eu_cast_instance_personnel">
|
||||
{{#each positions}}
|
||||
<div class="card position_instance" data-position-instance-id="{{position_instance_id}}"
|
||||
style="margin-bottom: 5px;">
|
||||
<div class="card-header">{{position_name}}
|
||||
<button class="iconbutton remove_position_instance_button" style="float: right;">
|
||||
<svg color="red" fill="currentColor" height="20" width="20">
|
||||
<use xlink:href="/img/bootstrap-icons.svg#trash"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="iconbutton restore_position_instance_button hide" style="float: right;">
|
||||
<svg fill="green" height="20" width="20">
|
||||
<use xlink:href="/img/bootstrap-icons.svg#arrow-counterclockwise"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<div class="col-12 eb_eu_instance_position_instance"
|
||||
data-instance-id="{{../instance_id}}"
|
||||
data-member-id="{{taken_by}}" data-member-name="{{taken_by_member}}"
|
||||
data-position-id="{{position_id}}">
|
||||
{{search2 type="member" classname="search_instance_position_instance" value_id=taken_by
|
||||
value=taken_by_member}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-form-label-sm col-3">Von: </label>
|
||||
<div class="input-group col-9">
|
||||
<input class="form-control form-control-sm eb_eu_instance_position_real_start_time qsf"
|
||||
type="datetime-local"
|
||||
value="{{real_start_time}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-form-label-sm col-3">Bis: </label>
|
||||
<div class="input-group col-9">
|
||||
<input class="form-control form-control-sm eb_eu_instance_position_real_end_time qsf"
|
||||
type="datetime-local"
|
||||
value="{{real_end_time}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
<div class="new_position_instances">
|
||||
<div class="card new_position_instance" style="margin-bottom: 5px;">
|
||||
<div class="card-header">Neue Personalposition
|
||||
<button class="iconbutton remove_new_position_instance_button" style="float: right;">
|
||||
<svg color="red" fill="currentColor" height="20" width="20">
|
||||
<use xlink:href="/img/bootstrap-icons.svg#trash"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="iconbutton restore_new_position_instance_button hide" style="float: right;">
|
||||
<svg fill="green" height="20" width="20">
|
||||
<use xlink:href="/img/bootstrap-icons.svg#arrow-counterclockwise"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row new_position_instance_position">
|
||||
<label class="col-form-label col-3">Position:</label>
|
||||
<div class="input-group col-9">
|
||||
{{search2 type="position" classname="search_new_position_instance_position"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row eb_eu_instance_new_position_instance_taken_by"
|
||||
data-instance-id="{{../instance_id}}">
|
||||
<label class="col-form-label col-3">Besetzung:</label>
|
||||
<div class="input-group col-9">
|
||||
{{search2 type="member" classname="search_new_position_instance_member"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-form-label-sm col-3">Von: </label>
|
||||
<div class="input-group col-9">
|
||||
<input class="form-control form-control-sm new_position_instance_real_start_time qsf"
|
||||
type="datetime-local"
|
||||
value="{{real_start_time}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-form-label-sm col-3">Bis: </label>
|
||||
<div class="input-group col-9">
|
||||
<input class="form-control form-control-sm new_position_instance_real_end_time qsf"
|
||||
type="datetime-local"
|
||||
value="{{real_end_time}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,91 @@
|
|||
<div class="instance col-lg-4" data-instance-id="{{instance_id}}"
|
||||
style="padding: 2px;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{{name}}
|
||||
<button class="btn btn-success eb_personnel_instance_save_btn" style="float: right;display:none;"
|
||||
type="button">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-4">Satz:</label>
|
||||
<div class="input-group col-8">
|
||||
<select class="form-control instance_select_billing_rate qsf">
|
||||
<option value="non-billable">nicht abrechenbar</option>
|
||||
{{#each billing_rates}}
|
||||
<option selected}}selected{{ title="{{description}}" value="{{billing_rate_id}}" {{#if
|
||||
/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-4">geplant:</label>
|
||||
<p class="col-8">
|
||||
{{planned_time}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-4">real:</label>
|
||||
<p class="col-8">
|
||||
{{real_time}}
|
||||
</p>
|
||||
</div>
|
||||
<h5>Personal:</h5>
|
||||
<div class="eu_cast_instance_personnel">
|
||||
{{#each positions}}
|
||||
<div class="card position_instance" data-position-instance-id="{{position_instance_id}}"
|
||||
style="margin-bottom: 5px;">
|
||||
<div class="card-header">{{position_name}}</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-4">Abrechenbar: </label>
|
||||
<div class="input-group col-8" style="align-items: center">
|
||||
<input billable}}checked{{
|
||||
class="eb_personnel_position_instance_is_billable qsf" type="checkbox" {{#if/if}}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-4">Besetzung: </label>
|
||||
<p class="col-8">{{taken_by_member}}</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-4">Einsatzzeit:</label>
|
||||
<p class="col-8">{{real_time}}</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-4">Einsatzstunden:</label>
|
||||
<p class="col-8">{{fulfilled_time}}</p>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-4">manuelle Abrechnung</label>
|
||||
<div class="input-group col-8" style="align-items: center">
|
||||
<input class="eb_personnel_position_instance_manual_billing_check qsf"
|
||||
type="checkbox">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row eb_personnel_position_instance_manual_billing_lump_sum_container hide">
|
||||
<label class="col-form-label col-4" title="Hier z.B. eine Fahrtkostenpauschale eintragen.">Pauschale:</label>
|
||||
<div class="input-group col-8" style="align-items: center">
|
||||
<input class="eb_personnel_position_instance_manual_billing_lump_sum qsf form-control"
|
||||
step="0.01" type="number" value="0.00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row hide eb_personnel_position_instance_manual_billing_money_for_time_container">
|
||||
<label class="col-form-label col-4"
|
||||
title="Hier z.B. das manuell berechnete Einsatzgeld eintragen.">Geld für
|
||||
Stunden:</label>
|
||||
<div class="input-group col-8" style="align-items: center">
|
||||
<input class="eb_personnel_position_instance_manual_billing_money_for_time qsf form-control"
|
||||
step="0.01" type="number" value="0.00">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -20,11 +20,12 @@
|
|||
<div class="form-group row eu_cast_instance_personal_position" data-instance-id="{{../instance_id}}"
|
||||
data-position-id="{{position_id}}" data-member-name="{{member_name}}"
|
||||
data-member-id="{{taken_by}}">
|
||||
<label class="col-4 col-form-label">{{name}}</label>
|
||||
<div class="input-group mb-3 col-8">
|
||||
{{> search base=this.base type="member"}}
|
||||
</div>
|
||||
<label class="col-4 col-form-label" position_description}}title="{{position_description}}" {{ {{#if/if}}>{{position_name}}</label>
|
||||
<div class="input-group mb-3 col-8">
|
||||
{{search2 type="member" classname="search_instance_position_member" value_id=taken_by
|
||||
value=member_name}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if vehicle_positions}}
|
||||
|
|
|
@ -85,6 +85,9 @@
|
|||
<textarea rows="4" class="form-control" id="other_intern">{{other_intern}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<input class="form-control hide" id="state" type="hidden" value="{{state}}"></input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
|
@ -35,6 +35,8 @@
|
|||
{{#if organiser.phone}}<p><b>Telefon: </b>{{organiser.phone}}</p>{{/if}}
|
||||
{{#if organiser.email}}<p><b>Email: </b>{{organiser.email}}</p>{{/if}}
|
||||
{{#if organiser.other}}<p><b>Sonstiges: </b>{{organiser.other}}</p>{{/if}}
|
||||
<h5>Abrechnung</h5>
|
||||
<p><b>Status:</b> {{state_name}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,14 +16,14 @@
|
|||
<div class="form-group row eu_cast_instance_personal_position" data-instance-id="{{../instance_id}}"
|
||||
data-position-id="{{position_id}}" data-member-name="{{member_name}}"
|
||||
data-member-id="{{taken_by}}">
|
||||
<label class="col-4 col-form-label">{{name}}</label>
|
||||
<label class="col-4 col-form-label" position_description}}title="{{position_description}}" {{ {{#if/if}}>{{position_name}}</label>
|
||||
<div class="input-group mb-3 col-8">
|
||||
{{#if taken_by}}{{member_name}}{{else}}
|
||||
<button class="btn btn-block btn-sm btn-secondary eu_cast_instance_self_register">Eintragen
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if vehicle_positions}}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<ul class="progressbar">
|
||||
<li class="progress_event_closed"><a href="/portal/eb/close_event?event_id={{event_id}}">Einsatz geschlossen</a>
|
||||
</li>
|
||||
<li class="progress_times_confirmed"><a href="/portal/eb/edit_times?event_id={{event_id}}">Einsatzzeiten
|
||||
bestätigt</a></li>
|
||||
<li class="progress_personnel_billing"><a href="/portal/eb/personnel_billing?event_id={{event_id}}">Personalabrechnung</a>
|
||||
</li>
|
||||
{{#each billing_states}}
|
||||
<li class="progress_approve_{{entity_id}}"><a
|
||||
href="/portal/eb/approve?stage={{entity_id}}&event_id={{../event_id}}">{{description}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
|
@ -269,6 +269,10 @@ button.modified {
|
|||
border-style: dashed !important;
|
||||
}
|
||||
|
||||
.card.modified {
|
||||
border-color: #ffca2c;
|
||||
}
|
||||
|
||||
.marked-for-delete {
|
||||
background-image: linear-gradient(136deg, #ffd9d9 13.64%, #ffffff 13.64%, #ffffff 50%, #ffd9d9 50%, #ffd9d9 63.64%, #ffffff 63.64%, #ffffff 100%);
|
||||
background-size: 15.84px 15.29px;
|
||||
|
@ -277,4 +281,88 @@ button.modified {
|
|||
/* Overwriting card-header min-height to unify header height if some cards have buttons inside */
|
||||
.card-header {
|
||||
min-height: 56px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 600px;
|
||||
margin: 100px auto;
|
||||
}
|
||||
|
||||
.progressbar {
|
||||
counter-reset: step;
|
||||
}
|
||||
|
||||
.progressbar li {
|
||||
list-style-type: none;
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
color: #7d7d7d;
|
||||
}
|
||||
|
||||
.progressbar li:before {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
content: counter(step);
|
||||
counter-increment: step;
|
||||
line-height: 30px;
|
||||
border: 2px solid #7d7d7d;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: 0 auto 10px auto;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.progressbar li:after {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: #7d7d7d;
|
||||
top: 15px;
|
||||
left: -50%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.progressbar li:first-child:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.progressbar li.active {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.progressbar li.active:before {
|
||||
border-color: #55b776;
|
||||
}
|
||||
|
||||
.progressbar li.active + li:after {
|
||||
background-color: #55b776;
|
||||
}
|
||||
|
||||
.nav-progressbar {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search2-result-overlay-list-entry {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -114,6 +114,7 @@ EventEditModule = (function () {
|
|||
event.name = $("#name").val();
|
||||
event.start = $("#start").val();
|
||||
event.end = $("#end").val();
|
||||
event.state = parseInt($("#state").val());
|
||||
if ($("#site").val().length > 0) {
|
||||
event.site = $("#site").val();
|
||||
}
|
||||
|
@ -140,6 +141,7 @@ EventEditModule = (function () {
|
|||
if ($("#contact_on_site_phone").val().length > 0) {
|
||||
event.contact_on_site_phone = $("#contact_on_site_phone").val();
|
||||
}
|
||||
console.log(event);
|
||||
$.ajax({
|
||||
url: '/api/events/' + event_id,
|
||||
type: 'PUT',
|
||||
|
@ -234,39 +236,6 @@ EventEditModule = (function () {
|
|||
}
|
||||
});
|
||||
|
||||
$(".eu_cast_instance_personal_position").each(function () {
|
||||
let pos = $(this).data("position-id");
|
||||
let instance = $(this).data("instance-id");
|
||||
|
||||
let callback = async function (caller) {
|
||||
let ms = this;
|
||||
let requirements_fulfilled = await check_position_requirements(pos, $(caller).data("entity-id"));
|
||||
if (!requirements_fulfilled) {
|
||||
$("#overwrite_position_requirements_modal").modal();
|
||||
$(".overwrite_position_requirements_modal_submit").off("click").on("click", function(){
|
||||
add_entity_to_position(instance, pos, $(caller).data("entity-id"));
|
||||
$("#"+ms.base+"-search").hide();
|
||||
$("#"+ms.base).val($(caller).data("firstname")+" "+$(caller).data("lastname")).attr("data-entity-id", $(caller).data("entity-id")).data("entity-id", $(caller).data("entity-id"));
|
||||
$("#"+ms.base+"_input_group").show();
|
||||
})
|
||||
}else{
|
||||
add_entity_to_position(instance, pos, $(caller).data("entity-id"));
|
||||
$("#"+ms.base+"-search").hide();
|
||||
$("#"+ms.base).val($(caller).data("firstname")+" "+$(caller).data("lastname")).attr("data-entity-id", $(caller).data("entity-id")).data("entity-id", $(caller).data("entity-id"));
|
||||
$("#"+ms.base+"_input_group").show();
|
||||
}
|
||||
};
|
||||
let delete_callback = function(){
|
||||
let instanceh = $(this.searchbar).closest(".instance").find(".card-header").first();
|
||||
if($(instanceh).hasClass("acs-green")){
|
||||
$(instanceh).removeClass("acs-green");
|
||||
$(instanceh).addClass("acs-red");
|
||||
}
|
||||
remove_entity_from_position(instance, pos);
|
||||
};
|
||||
var member_search = new MiniSearchbar("search_"+$(this).data("instance-id")+"_"+$(this).data("position-id"), callback, $(this).data("member-id"), $(this).data("member-name"), delete_callback);
|
||||
member_search.setup();
|
||||
});
|
||||
$(".eu_cast_instance_vehicle_position").each(function(){
|
||||
let pos = $(this).data("position-id");
|
||||
let instance = $(this).data("instance-id");
|
||||
|
@ -274,16 +243,44 @@ EventEditModule = (function () {
|
|||
let callback = function(caller){
|
||||
let ms = this;
|
||||
$("#"+ms.base+"-search").hide();
|
||||
$("#"+ms.base).val($(caller).data("identifier")).attr("data-entity-id", $(caller).data("entity-id")).data("entity-id", $(caller).data("entity-id"));
|
||||
$("#"+ms.base+"_input_group").show();
|
||||
$("#" + ms.base).val($(caller).data("identifier")).attr("data-entity-id", $(caller).data("entity-id")).data("entity-id", $(caller).data("entity-id"));
|
||||
$("#" + ms.base + "_input_group").show();
|
||||
add_entity_to_position(instance, pos, $(caller).data("entity-id"))
|
||||
};
|
||||
let delete_callback = function(){
|
||||
let delete_callback = function () {
|
||||
remove_entity_from_position(instance, pos);
|
||||
};
|
||||
var vehicle_search = new MiniSearchbar("search_"+$(this).data("instance-id")+"_"+$(this).data("position-id"), callback, $(this).data("entity-id"), $(this).data("identifier"), delete_callback);
|
||||
var vehicle_search = new MiniSearchbar("search_" + $(this).data("instance-id") + "_" + $(this).data("position-id"), callback, $(this).data("entity-id"), $(this).data("identifier"), delete_callback);
|
||||
vehicle_search.setup();
|
||||
});
|
||||
Search2.setup();
|
||||
$(".search_instance_position_member").off("change").on("change", async function () {
|
||||
let search = $(this).closest(".search2");
|
||||
let position_id = search.closest(".eu_cast_instance_personal_position").data("position-id") || undefined;
|
||||
let instance_id = search.closest(".eu_cast_instance_personal_position").data("instance-id") || undefined;
|
||||
let member_id = $(this).data("value-id");
|
||||
|
||||
if (member_id && $(this).val()) {
|
||||
//Added
|
||||
let requirements_fulfilled = await check_position_requirements(position_id, member_id);
|
||||
if (!requirements_fulfilled) {
|
||||
$("#overwrite_position_requirements_modal").modal();
|
||||
$(".overwrite_position_requirements_modal_submit").off("click").on("click", function () {
|
||||
add_entity_to_position(instance_id, position_id, member_id);
|
||||
})
|
||||
} else {
|
||||
add_entity_to_position(instance_id, position_id, member_id);
|
||||
}
|
||||
} else {
|
||||
let instanceh = $(this).closest(".instance");
|
||||
if ($(instanceh).hasClass("acs-green")) {
|
||||
$(instanceh).removeClass("acs-green");
|
||||
$(instanceh).addClass("acs-red");
|
||||
}
|
||||
remove_entity_from_position(instance_id, position_id);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -300,7 +297,7 @@ EventEditModule = (function () {
|
|||
}
|
||||
};
|
||||
let deactivate_modified = function () {
|
||||
|
||||
//TODO
|
||||
};
|
||||
let save = async function () {
|
||||
let data = {};
|
||||
|
|
|
@ -121,15 +121,27 @@ EventListModule = ( function() {
|
|||
}
|
||||
|
||||
let date = new Date(event.start);
|
||||
event.timeframe = ('0' + date.getDate()).slice(-2) + '.' + ('0' + (date.getMonth()+1)).slice(-2) + '.' + date.getFullYear() + ' ' + ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ' - ';
|
||||
event.timeframe = ('0' + date.getDate()).slice(-2) + '.' + ('0' + (date.getMonth() + 1)).slice(-2) + '.' + date.getFullYear() + ' ' + ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ' - ';
|
||||
let date2 = new Date(event.end);
|
||||
if(date.getDate() === date2.getDate()){
|
||||
if (date.getDate() === date2.getDate()) {
|
||||
event.timeframe += ('0' + date2.getHours()).slice(-2) + ':' + ('0' + date2.getMinutes()).slice(-2)
|
||||
}else{
|
||||
event.timeframe += event.timeframe = ('0' + date2.getDate()).slice(-2) + '.' + ('0' + (date2.getMonth()+1)).slice(-2) + '.' + date2.getFullYear() + ' ' + ('0' + date2.getHours()).slice(-2) + ':' + ('0' + date2.getMinutes()).slice(-2);
|
||||
} else {
|
||||
event.timeframe += event.timeframe = ('0' + date2.getDate()).slice(-2) + '.' + ('0' + (date2.getMonth() + 1)).slice(-2) + '.' + date2.getFullYear() + ' ' + ('0' + date2.getHours()).slice(-2) + ':' + ('0' + date2.getMinutes()).slice(-2);
|
||||
}
|
||||
|
||||
event.cast_status = await load_event_cast_status(event.entity_id);
|
||||
event.state_name = "unbekannt";
|
||||
if (event.state === 2) {
|
||||
event.state_name = "Einsatz geöffnet";
|
||||
} else if (event.state === 4) {
|
||||
event.state_name = "Einsatz geschlossen";
|
||||
} else if (event.state === 6) {
|
||||
event.state_name = "Einsatzzeiten bestätigt";
|
||||
} else if (event.state === 7) {
|
||||
event.state_name = "Personalabrechnung abgeschlossen";
|
||||
} else if (event.state === 8) {
|
||||
event.state_name = "Abrechnung abgeschlossen";
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
|
|
@ -0,0 +1,752 @@
|
|||
let searchParams;
|
||||
|
||||
$(document).ready(async function () {
|
||||
searchParams = new URLSearchParams(window.location.search);
|
||||
event_id = searchParams.get('event_id');
|
||||
await EventBilling.setup(event_id);
|
||||
});
|
||||
|
||||
EventBilling = (function () {
|
||||
let templates = {};
|
||||
let event = {};
|
||||
let billing_states = {};
|
||||
let event_min_billing_state = {};
|
||||
let pending_requests = [];
|
||||
|
||||
let setup = async function (event_id) {
|
||||
await load_templates();
|
||||
await load_event(event_id);
|
||||
await setup_pagination();
|
||||
let pathname = window.location.pathname;
|
||||
if (pathname === "/portal/eb/event") {
|
||||
await event_view.show_action_buttons();
|
||||
} else if (pathname === "/portal/eb/close_event") {
|
||||
await close_event_view.setup();
|
||||
} else if (pathname === "/portal/eb/edit_times") {
|
||||
await edit_times_view.setup();
|
||||
} else if (pathname === "/portal/eb/personnel_billing") {
|
||||
await personnel_billing_view.setup();
|
||||
} else if (pathname === "/portal/eb/approve") {
|
||||
await approve_view.setup();
|
||||
}
|
||||
};
|
||||
let run_pending_requests = async function (success_callback) {
|
||||
//Run all pending requests:
|
||||
await Promise.all(pending_requests.map(req => $.ajax(req))).then(async function (values) {
|
||||
pending_requests = []; //Clear pending requests
|
||||
let error_occured = false;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
let val = values[i];
|
||||
if (!is_ok(val)) {
|
||||
error_occured = true;
|
||||
}
|
||||
}
|
||||
if (!error_occured) {
|
||||
success_callback();
|
||||
}
|
||||
}, function () {
|
||||
//Failure
|
||||
});
|
||||
};
|
||||
let setup_pagination = async function () {
|
||||
await Promise.all([load_billing_states(), load_min_billing_states_for_event(event.entity_id)]).then(function (res) {
|
||||
billing_states = res[0];
|
||||
event_min_billing_state = res[1];
|
||||
});
|
||||
|
||||
let progressbar = {};
|
||||
progressbar.billing_states = billing_states;
|
||||
progressbar.event_id = event.entity_id;
|
||||
|
||||
$(".nav-progressbar").html(templates.progressbar(progressbar));
|
||||
$(".action_btn").html(templates.eb_action_buttons({event: event, states: billing_states}));
|
||||
$(".progressbar > li").css("width", (100 / (3 + billing_states.length)) + "%");
|
||||
//Mark active
|
||||
if (event.state >= 4) {
|
||||
$(".progress_event_closed").addClass("active");
|
||||
}
|
||||
if (event.state >= 6) {
|
||||
$(".progress_times_confirmed").addClass("active");
|
||||
}
|
||||
if (event.state >= 7) {
|
||||
$(".progress_personnel_billing").addClass("active");
|
||||
|
||||
for (let state of billing_states) {
|
||||
$(".progress_approve_" + state.entity_id).addClass("active");
|
||||
if (state.entity_id === event_min_billing_state) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let load_billing_states = async function () {
|
||||
const res = $.ajax({
|
||||
url: '/api/billing_states',
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
error: function () {
|
||||
alert("Es ist ein Fehler aufgetreten!");
|
||||
}
|
||||
});
|
||||
if (is_ok(res)) {
|
||||
return res;
|
||||
}
|
||||
};
|
||||
let load_min_billing_states_for_event = async function (event_id) {
|
||||
const res = $.ajax({
|
||||
url: '/api/events/' + event_id + '/billing_states/min',
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
error: function () {
|
||||
alert("Es ist ein Fehler aufgetreten!");
|
||||
}
|
||||
});
|
||||
if (is_ok(res)) {
|
||||
return res;
|
||||
}
|
||||
};
|
||||
let load_templates = async function () {
|
||||
const progressbar = $.get("/templates/progressbar.hbs");
|
||||
const eb_action_buttons = $.get("/templates/eb_action_buttons.hbs");
|
||||
await Promise.all([progressbar, eb_action_buttons]).then(function (res) {
|
||||
templates.progressbar = Handlebars.compile(res[0]);
|
||||
templates.eb_action_buttons = Handlebars.compile(res[1]);
|
||||
});
|
||||
|
||||
};
|
||||
let load_event = async function (event_id) {
|
||||
await $.ajax({
|
||||
url: '/api/events/' + event_id,
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
success: function (data) {
|
||||
if (is_ok(data)) {
|
||||
event = data;
|
||||
}
|
||||
},
|
||||
timeout: 3000,
|
||||
error: function () {
|
||||
alert("Es ist ein Fehler aufgetreten!");
|
||||
}
|
||||
});
|
||||
};
|
||||
let load_eu_instances = async function () {
|
||||
const res = $.ajax({
|
||||
url: '/api/events/' + event.entity_id + '/instances',
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
error: function () {
|
||||
alert("Es ist ein Fehler aufgetreten!");
|
||||
}
|
||||
});
|
||||
if (is_ok(res)) {
|
||||
return res;
|
||||
} else {
|
||||
throw res;
|
||||
}
|
||||
};
|
||||
let load_eu_instance_positions = async function (instances) {
|
||||
let queue = [];
|
||||
for (let instance of instances) {
|
||||
queue.push(load_positions_for_instance_new(instance.instance_id));
|
||||
}
|
||||
await Promise.all(queue).then(function (res) {
|
||||
for (let i = 0; i < instances.length; i++) {
|
||||
instances[i].positions = res[i];
|
||||
}
|
||||
});
|
||||
};
|
||||
let update_position_instance = function (data) {
|
||||
pending_requests.push(({
|
||||
type: "PATCH",
|
||||
url: "/api/events/position_instances/" + data.position_instance_id,
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
data: JSON.stringify(data),
|
||||
}));
|
||||
};
|
||||
|
||||
//TODO: only call if instance really changed
|
||||
let save_eu_instance = function (data) {
|
||||
pending_requests.push(({
|
||||
type: "PATCH",
|
||||
url: "/api/events/" + event_id + "/instances/" + data.instance_id,
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
data: JSON.stringify(data),
|
||||
error: function () {
|
||||
alert("Es ist ein Fehler aufgetreten.");
|
||||
},
|
||||
}));
|
||||
};
|
||||
let event_view = (function () {
|
||||
let show_action_buttons = async function () {
|
||||
//Show action button
|
||||
if (event.state < 4) {
|
||||
$(".goto_close_event").show();
|
||||
} else if (event.state === 4) {
|
||||
$(".goto_edit_times").show();
|
||||
} else if (event.state === 6) {
|
||||
$(".goto_personnel_billing").show();
|
||||
} else if (event.state >= 7) {
|
||||
let min_billing_state;
|
||||
if (!event_min_billing_state.error) {
|
||||
min_billing_state = event_min_billing_state;
|
||||
} else {
|
||||
min_billing_state = billing_states[0].entity_id;
|
||||
}
|
||||
//Show next approval step (billing_state) button
|
||||
for (let i = 0; i < billing_states.length - 1; i++) {
|
||||
if (billing_states[i].entity_id === min_billing_state) {
|
||||
$(".goto_approve_" + billing_states[i + 1].entity_id).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
show_action_buttons
|
||||
}
|
||||
}());
|
||||
let close_event_view = (function () {
|
||||
let setup = function () {
|
||||
$(".close_event_btn").off("click").on("click", async function () {
|
||||
try {
|
||||
await close_event();
|
||||
window.location = "/portal/eb/edit_times?event_id=" + event.entity_id;
|
||||
} catch (e) {
|
||||
//TODO: proper error reporting
|
||||
}
|
||||
});
|
||||
};
|
||||
let close_event = async function () {
|
||||
const res = $.ajax({
|
||||
url: '/api/events/' + event.entity_id + '/close',
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
error: function () {
|
||||
alert("Es ist ein Fehler aufgetreten!");
|
||||
}
|
||||
});
|
||||
if (is_ok(res)) {
|
||||
return res;
|
||||
} else {
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
return {
|
||||
setup
|
||||
}
|
||||
}());
|
||||
let edit_times_view = (function () {
|
||||
let eu_instances = {};
|
||||
let load_local_templates = async function () {
|
||||
const eu_instance_card = $.get("/templates/eb_eu_instance_card.hbs");
|
||||
await Promise.all([eu_instance_card]).then(function (res) {
|
||||
templates.eu_instance_card = Handlebars.compile(res[0]);
|
||||
});
|
||||
};
|
||||
let setup = async function () {
|
||||
await load_local_templates();
|
||||
eu_instances = await load_eu_instances(event.entity_id);
|
||||
await load_eu_instance_positions(eu_instances);
|
||||
|
||||
$(".eu_instances_container").empty();
|
||||
|
||||
for (let instance of eu_instances) {
|
||||
if (instance.planned_start_time && instance.planned_end_time) {
|
||||
instance.planned_time = combine_start_end_time(instance.planned_start_time, instance.planned_end_time);
|
||||
}
|
||||
|
||||
for (let position of instance.positions) {
|
||||
if (position.taken_by) {
|
||||
let member = await get_member(position.taken_by);
|
||||
position.taken_by_member = member.firstname + " " + member.lastname;
|
||||
}
|
||||
}
|
||||
|
||||
$(".eu_instances_container").append(templates.eu_instance_card(instance));
|
||||
console.log(instance);
|
||||
|
||||
}
|
||||
|
||||
Search2.setup(); //Setup searchbars
|
||||
|
||||
$(".eb_eu_instance_save_btn").off("click").on("click", save);
|
||||
|
||||
//Apply remove/restore position_instance button listeners
|
||||
$(".remove_position_instance_button").off("click").on("click", delete_position_btn);
|
||||
$(".remove_new_position_instance_button").off("click").on("click", function () {
|
||||
$(this).closest(".card").remove();
|
||||
});
|
||||
$(".restore_position_instance_button").off("click").on("click", restore_position_btn);
|
||||
|
||||
$(".approve_times_btn").off("click").on("click", approve_times);
|
||||
$(".qsf").off("change").on("change", function () { //Show save button if instance changed
|
||||
$(this).closest(".position_instance").addClass("modified");
|
||||
$(this).closest(".instance").find(".eb_eu_instance_save_btn").show();
|
||||
})
|
||||
};
|
||||
let save = async function () {
|
||||
//TODO: use global save_pending_requests()
|
||||
//TODO: send requests after each step. Otherwise request to delete something get's executed before request to edit same thing
|
||||
pending_requests = [];
|
||||
|
||||
let parent = $(this).closest(".instance");
|
||||
let save_btn = $(this);
|
||||
|
||||
//Save basic instance data:
|
||||
let instance_patch = {};
|
||||
instance_patch.instance_id = parent.data("instance-id") || undefined; //undefined = do not transmit at all
|
||||
instance_patch.real_start_time = parent.find(".eb_eu_instance_real_start_time").val() || null; //null = set to null in database
|
||||
instance_patch.real_end_time = parent.find(".eb_eu_instance_real_end_time").val() || null;
|
||||
save_eu_instance(instance_patch);
|
||||
|
||||
//Save newly added positions:
|
||||
let npi = $(".new_position_instance");
|
||||
for (let i = 0; i < npi.length; i++) {
|
||||
let new_pos = $(npi[i]);
|
||||
let position = new_pos.find(".search_new_position_instance_position").data("value-id") || undefined;
|
||||
let member = new_pos.find(".search_new_position_instance_member").data("value-id") || undefined;
|
||||
console.log("member:" + member + " position:" + position);
|
||||
//Skip new position if position or member isn't set
|
||||
// TODO: simplify
|
||||
if (!position || !member || position.trim() === "" || member.trim() === "") {
|
||||
//Report error if new position missing either position or member (but not both)
|
||||
if ((!position || position.trim() === "") && member) {
|
||||
alert("Der neuen Personalposition fehlt die Positionsangabe!");
|
||||
return;
|
||||
}
|
||||
if ((!member || member.trim() === "") && position) {
|
||||
alert("Der neuen Personalposition fehlt ein Mitglied!");
|
||||
return;
|
||||
}
|
||||
|
||||
//Skip empty new positions
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_position_instance = {};
|
||||
new_position_instance.instance_id = parent.data("instance-id") || undefined;
|
||||
new_position_instance.position_id = position || undefined;
|
||||
new_position_instance.taken_by = member || undefined;
|
||||
new_position_instance.real_start_time = new_pos.find(".new_position_instance_real_start_time").val() || undefined;
|
||||
new_position_instance.real_end_time = new_pos.find(".new_position_instance_real_end_time").val() || undefined;
|
||||
|
||||
add_position_to_position_instances(new_position_instance);
|
||||
}
|
||||
|
||||
//Remove position instances which are marked for removal:
|
||||
let to_removed = $(".marked-for-delete");
|
||||
|
||||
for (i = 0; i < to_removed.length; i++) {
|
||||
let element = $(to_removed[i]);
|
||||
let position_instance_id = element.closest(".position_instance").data("position-instance-id");
|
||||
console.log("removing: " + position_instance_id);
|
||||
delete_position_instance(position_instance_id);
|
||||
to_removed[i].remove();
|
||||
}
|
||||
|
||||
//Update changed position instances:
|
||||
let to_updated = $(".position_instance.modified");
|
||||
for (i = 0; i < to_updated.length; i++) {
|
||||
let element = $(to_updated[i]);
|
||||
let update_set = {};
|
||||
|
||||
update_set.position_instance_id = element.data("position-instance-id") || undefined;
|
||||
update_set.taken_by = element.find(".search_instance_position_instance").data("value-id") || null;
|
||||
update_set.real_start_time = element.find(".eb_eu_instance_position_real_start_time").val() || null;
|
||||
update_set.real_end_time = element.find(".eb_eu_instance_position_real_end_time").val() || null;
|
||||
console.log(update_set);
|
||||
update_position_instance(update_set);
|
||||
}
|
||||
|
||||
//Run all pending requests:
|
||||
await Promise.all(pending_requests.map(req => $.ajax(req))).then(async function (values) {
|
||||
let error_occured = false;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
let val = values[i];
|
||||
if (!is_ok(val)) {
|
||||
error_occured = true;
|
||||
}
|
||||
}
|
||||
if (!error_occured) {
|
||||
save_btn.hide();
|
||||
await setup();
|
||||
}
|
||||
}, function () {
|
||||
//Failure
|
||||
});
|
||||
|
||||
pending_requests = []; //Clear pending requests
|
||||
};
|
||||
let add_position_to_position_instances = function (data) {
|
||||
pending_requests.push(({
|
||||
type: "POST",
|
||||
url: "/api/events/instances/" + data.instance_id + "/positions/" + data.position_id,
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
data: JSON.stringify(data),
|
||||
}));
|
||||
};
|
||||
let delete_position_instance = function (position_instance_id) {
|
||||
pending_requests.push(({
|
||||
type: "DELETE",
|
||||
url: "/api/events/position_instances/" + position_instance_id,
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
}));
|
||||
};
|
||||
let delete_position_btn = function () {
|
||||
let position = $(this).closest(".card");
|
||||
|
||||
$(this).closest(".instance").find(".eb_eu_instance_save_btn").show(); //Show save button
|
||||
|
||||
//Hide delete button and show restore button
|
||||
$(this).hide();
|
||||
position.find(".restore_position_instance_button").show();
|
||||
|
||||
$(position).addClass("marked-for-delete");
|
||||
};
|
||||
let restore_position_btn = function () {
|
||||
let position = $(this).closest(".card");
|
||||
|
||||
//Hide restore button and show delete button
|
||||
$(this).hide();
|
||||
position.find(".remove_position_instance_button").show();
|
||||
|
||||
$(position).removeClass("marked-for-delete");
|
||||
};
|
||||
let approve_times = async function () {
|
||||
let save_btns = $(".eb_eu_instance_save_btn:visible");
|
||||
if (save_btns.length !== 0) {
|
||||
alert("Es gibt ungespeicherte Änderungen. Bitte speichere alle Änderungen bevor zu die Einsatzzeiten bestätigst.");
|
||||
} else {
|
||||
|
||||
const res = $.ajax({
|
||||
url: '/api/events/' + event.entity_id + '/approve_times',
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
error: function () {
|
||||
alert("Es ist ein Fehler aufgetreten!");
|
||||
}
|
||||
});
|
||||
if (is_ok(res)) {
|
||||
window.location.href = "/portal/eb/personnel_billing?event_id=" + event.entity_id;
|
||||
} else {
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
};
|
||||
return {
|
||||
setup
|
||||
}
|
||||
}());
|
||||
let personnel_billing_view = (function () {
|
||||
let eu_instances = {};
|
||||
let setup = async function () {
|
||||
await load_local_templates();
|
||||
eu_instances = await load_eu_instances();
|
||||
await load_eu_instance_positions(eu_instances);
|
||||
let billing_rates = await load_personnel_billing_rates();
|
||||
|
||||
$(".eu_instances_container").empty();
|
||||
|
||||
for (let instance of eu_instances) {
|
||||
if (instance.planned_start_time && instance.planned_end_time) {
|
||||
instance.planned_time = combine_start_end_time(instance.planned_start_time, instance.planned_end_time);
|
||||
}
|
||||
if (instance.real_start_time && instance.real_end_time) {
|
||||
instance.real_time = combine_start_end_time(instance.real_start_time, instance.real_end_time);
|
||||
}
|
||||
|
||||
instance.billing_rates = JSON.parse(JSON.stringify(billing_rates)); //Clone array (not copy with reference) to set selected rate
|
||||
|
||||
for (let rate of instance.billing_rates) {
|
||||
if (rate.billing_rate_id === instance.billing_rate_id) {
|
||||
rate.selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (let position of instance.positions) {
|
||||
if (position.taken_by) {
|
||||
let member = await get_member(position.taken_by);
|
||||
position.taken_by_member = member.firstname + " " + member.lastname;
|
||||
}
|
||||
position.real_time = combine_start_end_time(position.real_start_time, position.real_end_time);
|
||||
if (position.billable === null) {
|
||||
//If billable is set to null in database set it to true (default) in UI
|
||||
position.billable = true;
|
||||
}
|
||||
}
|
||||
|
||||
$(".eu_instances_container").append(templates.personnel_instance_card(instance));
|
||||
|
||||
//When non-billable is active disable billable and manual billing checkboxes
|
||||
$(".instance_select_billing_rate").each(function () {
|
||||
if ($(this).val() === "non-billable") {
|
||||
$(this).closest(".instance").find(".eb_personnel_position_instance_is_billable").prop("checked", false).prop("disabled", true);
|
||||
$(this).closest(".instance").find(".eb_personnel_position_instance_manual_billing_check").prop("checked", false).prop("disabled", true);
|
||||
}
|
||||
})
|
||||
|
||||
console.log(instance); //TODO: remove when testing done
|
||||
}
|
||||
|
||||
$(".instance_select_billing_rate").on("change", function () {
|
||||
let val = $(this).val();
|
||||
let instance = $(this).closest(".instance");
|
||||
|
||||
if (val === "non-billable") {
|
||||
instance.find(".eb_personnel_position_instance_is_billable").prop("checked", false).prop("disabled", true);
|
||||
instance.find(".eb_personnel_position_instance_manual_billing_check").prop("checked", false).prop("disabled", true);
|
||||
} else {
|
||||
instance.find(".eb_personnel_position_instance_is_billable").prop("disabled", false);
|
||||
instance.find(".eb_personnel_position_instance_manual_billing_check").prop("disabled", false);
|
||||
}
|
||||
});
|
||||
$(".eb_personnel_position_instance_manual_billing_check").on("change", function () {
|
||||
let personal_instance = $(this).closest(".position_instance");
|
||||
if ($(this).prop("checked")) {
|
||||
personal_instance.find(".eb_personnel_position_instance_manual_billing_money_for_time_container").show();
|
||||
personal_instance.find(".eb_personnel_position_instance_manual_billing_lump_sum_container").show();
|
||||
} else {
|
||||
personal_instance.find(".eb_personnel_position_instance_manual_billing_money_for_time_container").hide();
|
||||
personal_instance.find(".eb_personnel_position_instance_manual_billing_money_for_time").val("0.00");
|
||||
personal_instance.find(".eb_personnel_position_instance_manual_billing_lump_sum_container").hide();
|
||||
personal_instance.find(".eb_personnel_position_instance_manual_billing_lump_sum").val("0.00");
|
||||
}
|
||||
});
|
||||
$(".qsf").on("change", function () {
|
||||
let instance = $(this).closest(".instance");
|
||||
instance.find(".eb_personnel_instance_save_btn").show();
|
||||
$(this).addClass("modified");
|
||||
});
|
||||
$(".eb_personnel_instance_save_btn").on("click", save);
|
||||
$(".check_personnel_billing_btn").on("click", async function () {
|
||||
await finish_personnel_billing();
|
||||
window.location = "/portal/eb/approve?event_id=" + event.entity_id + "&stage=" + billing_states[0].entity_id;
|
||||
})
|
||||
};
|
||||
let load_local_templates = async function () {
|
||||
const personnel_instance_card = $.get("/templates/eb_personnel_instance_card.hbs");
|
||||
await Promise.all([personnel_instance_card]).then(function (res) {
|
||||
templates.personnel_instance_card = Handlebars.compile(res[0]);
|
||||
});
|
||||
};
|
||||
let load_personnel_billing_rates = async function () {
|
||||
const res = $.ajax({
|
||||
url: '/api/personnel_billing_rates',
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
error: function () {
|
||||
alert("Es ist ein Fehler aufgetreten!");
|
||||
}
|
||||
});
|
||||
if (is_ok(res)) {
|
||||
return res;
|
||||
}
|
||||
};
|
||||
let save = async function () {
|
||||
let instance = $(this).closest(".instance");
|
||||
pending_requests = [];
|
||||
|
||||
//Save Instance:
|
||||
|
||||
let instance_patch = {};
|
||||
instance_patch.instance_id = instance.data("instance-id") || undefined;
|
||||
|
||||
let billing_rate = instance.find(".instance_select_billing_rate").val();
|
||||
|
||||
//Remove personnel billing entries if billing_rate was set to non-billable
|
||||
if (billing_rate === "non-billable") {
|
||||
instance.find(".position_instance").each(function () {
|
||||
remove_personnel_billing_entry($(this).data("position-instance-id"));
|
||||
});
|
||||
instance_patch.billing_rate_id = null;
|
||||
} else {
|
||||
instance_patch.billing_rate_id = billing_rate;
|
||||
}
|
||||
save_eu_instance(instance_patch);
|
||||
run_pending_requests(function () {
|
||||
});
|
||||
|
||||
//Save position instances:
|
||||
for (let position_instance of instance.find(".position_instance")) {
|
||||
let position_instance_id = $(position_instance).data("position-instance-id") || undefined;
|
||||
|
||||
//Patch eu_position_instance table
|
||||
let position_instance_patch = {};
|
||||
position_instance_patch.position_instance_id = position_instance_id;
|
||||
if (billing_rate === "non-billable") {
|
||||
position_instance_patch.billable = false;
|
||||
} else {
|
||||
position_instance_patch.billable = $(position_instance).find(".eb_personnel_position_instance_is_billable").prop("checked");
|
||||
}
|
||||
update_position_instance(position_instance_patch)
|
||||
}
|
||||
run_pending_requests(function () {
|
||||
});
|
||||
|
||||
for (let position_instance of instance.find(".position_instance")) {
|
||||
let position_instance_id = $(position_instance).data("position-instance-id") || undefined;
|
||||
|
||||
if ($(position_instance).find(".eb_personnel_position_instance_manual_billing_check").prop("checked")) {
|
||||
//manual billing
|
||||
let personnel_billing_patch = {};
|
||||
personnel_billing_patch.position_instance_id = position_instance_id;
|
||||
personnel_billing_patch.money_from_lump_sum = $(position_instance).find(".eb_personnel_position_instance_manual_billing_lump_sum").val() || undefined;
|
||||
personnel_billing_patch.money_for_time = $(position_instance).find(".eb_personnel_position_instance_manual_billing_money_for_time").val() || undefined;
|
||||
add_personnel_billing_entry_manual(personnel_billing_patch);
|
||||
} else {
|
||||
if (billing_rate !== "non-billable" && $(position_instance).find(".eb_personnel_position_instance_is_billable").prop("checked")) {
|
||||
//Add personnel_billing entry. Overwrites existing entries
|
||||
add_personnel_billing_entry(position_instance_id, billing_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
run_pending_requests(function () {
|
||||
instance.find(".eb_personnel_instance_save_btn").hide();
|
||||
instance.find(".modified").removeClass("modified");
|
||||
});
|
||||
|
||||
};
|
||||
let remove_personnel_billing_entry = function (position_instance_id) {
|
||||
pending_requests.push(({
|
||||
type: "DELETE",
|
||||
url: "/api/personnel_billing/" + position_instance_id,
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
}));
|
||||
};
|
||||
let add_personnel_billing_entry = function (position_instance_id2, billing_rate_id2) {
|
||||
let data = {
|
||||
position_instance_id: position_instance_id2,
|
||||
billing_rate_id: billing_rate_id2
|
||||
};
|
||||
pending_requests.push(({
|
||||
type: "POST",
|
||||
url: "/api/personnel_billing/",
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
data: JSON.stringify(data)
|
||||
}));
|
||||
};
|
||||
let add_personnel_billing_entry_manual = function (data) {
|
||||
pending_requests.push(({
|
||||
type: "POST",
|
||||
url: "/api/personnel_billing/",
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
data: JSON.stringify(data),
|
||||
}));
|
||||
};
|
||||
let finish_personnel_billing = async function () {
|
||||
const res = $.ajax({
|
||||
url: '/api/events/' + event.entity_id + '/finish_personnel_billing',
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
error: function () {
|
||||
alert("Es ist ein Fehler aufgetreten!");
|
||||
}
|
||||
});
|
||||
if (is_ok(res)) {
|
||||
return res;
|
||||
} else {
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
return {
|
||||
setup
|
||||
}
|
||||
}());
|
||||
let approve_view = (function () {
|
||||
let setup = async function () {
|
||||
await load_local_templates();
|
||||
|
||||
let instances = await load_eu_instances();
|
||||
await load_eu_instance_positions(instances);
|
||||
|
||||
let total_sum = 0;
|
||||
|
||||
for (let instance of instances) {
|
||||
instance.instance_total_sum = 0;
|
||||
for (let pos of instance.positions) {
|
||||
let val = await load_personnel_billing(pos.position_instance_id);
|
||||
if (val) {
|
||||
val.total_money = Number(val.total_money).toFixed(2);
|
||||
instance.instance_total_sum = Number(Number(instance.instance_total_sum) + Number(val.total_money)).toFixed(2);
|
||||
val.money_from_lump_sum = Number(val.money_from_lump_sum).toFixed(2);
|
||||
val.money_for_time = Number(val.money_for_time).toFixed(2);
|
||||
pos.personnel_billing = val;
|
||||
}
|
||||
if (pos.taken_by) {
|
||||
let member = await get_member(pos.taken_by);
|
||||
pos.taken_by_member = member.firstname + " " + member.lastname;
|
||||
}
|
||||
|
||||
pos.real_time = combine_start_end_time(pos.real_start_time, pos.real_end_time);
|
||||
}
|
||||
|
||||
total_sum = Number(Number(total_sum) + Number(instance.instance_total_sum)).toFixed(2);
|
||||
|
||||
$(".eu_approve_summary").append(templates.approve_instance_card(instance));
|
||||
}
|
||||
console.log("Gesamt gesamt:" + total_sum);
|
||||
$(".eu_approve_summary").append(templates.approve_total_sum({total_sum}));
|
||||
$(".approve_btn").on("click", async function () {
|
||||
let current_state = searchParams.get('stage') || undefined;
|
||||
if (is_ok(await approve(current_state))) {
|
||||
window.location = "/portal/eb/event?event_id=" + event.entity_id;
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
let load_local_templates = async function () {
|
||||
const approve_instance_card = $.get("/templates/eb_approve_instance_card.hbs");
|
||||
const approve_total_sum = $.get("/templates/eb_approve_total_sum.hbs");
|
||||
await Promise.all([approve_instance_card, approve_total_sum]).then(function (res) {
|
||||
templates.approve_instance_card = Handlebars.compile(res[0]);
|
||||
templates.approve_total_sum = Handlebars.compile(res[1]);
|
||||
});
|
||||
};
|
||||
let load_personnel_billing = async function (position_instance_id) {
|
||||
const data = await $.ajax({
|
||||
url: '/api/personnel_billing/' + position_instance_id,
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
});
|
||||
if (!is_ok(data)) {
|
||||
show_error(data)
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
let approve = async function (stage_id) {
|
||||
const res = $.ajax({
|
||||
url: '/api/events/' + event.entity_id + '/approve?stage=' + stage_id,
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
error: function () {
|
||||
alert("Es ist ein Fehler aufgetreten!");
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
return {
|
||||
setup
|
||||
}
|
||||
}());
|
||||
return {
|
||||
setup
|
||||
}
|
||||
}());
|
|
@ -151,9 +151,25 @@ let load_positions_for_instance = async function(instance_id){
|
|||
return res;
|
||||
}
|
||||
};
|
||||
let load_vehicle_positions_for_instance = async function(instance_id){
|
||||
let load_positions_for_instance_new = async function (instance_id) {
|
||||
const res = $.ajax({
|
||||
url: '/api/events/instances/' + instance_id + '/positions',
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
error: function () {
|
||||
alert("Verbindung zum Server unterbrochen!");
|
||||
}
|
||||
});
|
||||
if (is_ok(res)) {
|
||||
return res;
|
||||
} else {
|
||||
throw res;
|
||||
}
|
||||
};
|
||||
let load_vehicle_positions_for_instance = async function (instance_id) {
|
||||
const res = await $.ajax({
|
||||
url: '/api/events/instances/'+instance_id+'/vehicle_positions',
|
||||
url: '/api/events/instances/' + instance_id + '/vehicle_positions',
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
timeout: 3000,
|
||||
|
@ -341,14 +357,29 @@ $(document).ajaxStart(function() {
|
|||
$(".loading_animation").show();
|
||||
});
|
||||
|
||||
$(document).ajaxStop(function() {
|
||||
$(document).ajaxStop(function () {
|
||||
$(".loading_animation").hide();
|
||||
});
|
||||
|
||||
let show_saved_animation = function(){
|
||||
let show_saved_animation = function () {
|
||||
$('.saved_animation').fadeIn(500).fadeOut(500);
|
||||
$(document).mousemove(function(e){
|
||||
$(".saved_animation").css({left:e.pageX+15, top:e.pageY});
|
||||
$(document).mousemove(function (e) {
|
||||
$(".saved_animation").css({left: e.pageX + 15, top: e.pageY});
|
||||
});
|
||||
$(document).off("mouseover");
|
||||
}
|
||||
};
|
||||
|
||||
let combine_start_end_time = function (start, end) {
|
||||
let date = new Date(start);
|
||||
let start_date = ('0' + date.getDate()).slice(-2) + '.' + ('0' + (date.getMonth() + 1)).slice(-2) + '.' + date.getFullYear();
|
||||
let start_time = ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ' - ';
|
||||
date = new Date(end);
|
||||
let end_date = ('0' + date.getDate()).slice(-2) + '.' + ('0' + (date.getMonth() + 1)).slice(-2) + '.' + date.getFullYear();
|
||||
let end_time = ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2);
|
||||
|
||||
if (start_date === end_date) {
|
||||
return start_date + " " + start_time + end_time;
|
||||
} else {
|
||||
return start_date + " " + start_time + end_date + " " + end_time;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ function MiniSearchbar(pbase, pcallback, value_id, value_name, delete_callback){
|
|||
this.delete_callback = delete_callback;
|
||||
|
||||
this.setup = function(){
|
||||
console.log('This function is deprecated. Please migrate to search2');
|
||||
var ms = this;
|
||||
|
||||
if(ms.value_id && ms.value_name){
|
||||
|
@ -58,7 +59,9 @@ function MiniSearchbar(pbase, pcallback, value_id, value_name, delete_callback){
|
|||
}
|
||||
$("#"+ms.base+"_remove").off("click").on("click", function(){
|
||||
ms.remove(ms);
|
||||
ms.delete_callback(this);
|
||||
if (ms.delete_callback) {
|
||||
ms.delete_callback(this);
|
||||
}
|
||||
});
|
||||
$('body').click(function(evt){
|
||||
if(evt.target.class === ms.base+"-search-result-overlay")
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
Handlebars.registerHelper('search2', function (arguments) {
|
||||
var args = new Map();
|
||||
|
||||
//Map arguments to args Map.
|
||||
Object.keys(arguments.hash).forEach(key => {
|
||||
var escapedKey = Handlebars.escapeExpression(key);
|
||||
var escapedValue = Handlebars.escapeExpression(arguments.hash[key]);
|
||||
|
||||
// Remove pairs with empty values
|
||||
if (escapedValue) {
|
||||
args.set(escapedKey, escapedValue);
|
||||
}
|
||||
});
|
||||
|
||||
//Check if type argument is set
|
||||
if (!args.get("type")) {
|
||||
console.error("Missing type for search2 Helper!");
|
||||
return new Handlebars.SafeString("<span style=\"color: red\">Konnte Suchfunktion nicht laden!</span>");
|
||||
}
|
||||
|
||||
var search_html = '<div class="search2">\n' +
|
||||
' <div class="input-group search2-search-input-group" ' + (args.get("value") && args.get("value_id") ? 'style="display: none;"' : '') + '>\n' +
|
||||
' <input autocomplete="off" class="form-control search2-searchbar" data-search-type="' + args.get("type") + '" type="text">\n' +
|
||||
' <span class="input-group-append">\n' +
|
||||
' <span class="btn btn-outline-secondary" type="button">\n' +
|
||||
' <svg width="16" height="16" fill="currentColor">\n' +
|
||||
' <use xlink:href="/img/bootstrap-icons.svg#search"></use>\n' +
|
||||
' </svg>\n' +
|
||||
' </span>\n' +
|
||||
' </span>\n' +
|
||||
' </div>\n' +
|
||||
' <div class="search2-result-overlay" style="display: none;">\n' +
|
||||
' <ul class="search2-result-overlay-list"></ul>\n' +
|
||||
' </div>\n' +
|
||||
' <div class="input-group search2-selected-input-group" ' + (!args.get("value") || !args.get("value_id") ? 'style="display: none;"' : '') + '>\n' +
|
||||
' <input disabled class="form-control qsf search2-selected ' + (args.get("classname") ? args.get("classname") : '') + '" type="text" ' + (args.get("value") && args.get("value_id") ? 'data-value-id="' + args.get("value_id") + '" value="' + args.get("value") + '"' : '') + '>\n' +
|
||||
' <span class="input-group-append">\n' +
|
||||
' <span class="btn btn-outline-secondary search2-remove-value-btn" type="button">\n' +
|
||||
' <svg width="16" height="16" fill="currentColor">\n' +
|
||||
' <use xlink:href="/img/bootstrap-icons.svg#pencil-square"></use>\n' +
|
||||
' </svg>\n' +
|
||||
' </span>\n' +
|
||||
' </span>\n' +
|
||||
' </div>\n' +
|
||||
'</div>';
|
||||
return new Handlebars.SafeString(search_html);
|
||||
});
|
||||
|
||||
Search2 = (function () {
|
||||
let setup = function () {
|
||||
$(".search2-remove-value-btn").off("click").on("click", remove_value);
|
||||
|
||||
//Hide search result overlay when user clicks
|
||||
$('body').off("click").on("click", function (evt) {
|
||||
if ($(evt.target).hasClass("search2-searchbar") || $(evt.target).closest(".search2-result-overlay").length)
|
||||
return;
|
||||
hide_overlay();
|
||||
});
|
||||
|
||||
//Setup event handlers for typing, clicking the searchbar, etc.
|
||||
$(".search2-searchbar").off("click").on("click", searchbar_onclick).off("keyup paste cut").on("keyup paste cut", searchbar_typing).off("mouseenter focusin").on("mouseenter focusin", show_overlay);
|
||||
|
||||
};
|
||||
let remove_value = function () {
|
||||
let search = $(this).closest(".search2");
|
||||
search.find(".search2-selected").val("").removeAttr("data-value-id"); //Remove stored value
|
||||
search.find(".search2-selected-input-group").hide();
|
||||
search.find(".search2-search-input-group").show();
|
||||
|
||||
//Fire change event manually to help detect changes
|
||||
const event = new Event('change');
|
||||
search.find(".search2-selected")[0].dispatchEvent(event);
|
||||
};
|
||||
let hide_overlay = function (target) {
|
||||
if (!target) { //Hide all overlays
|
||||
$(".search2-result-overlay").hide();
|
||||
} else { //Hide only specific overlay
|
||||
let searchbar = $(target);
|
||||
searchbar.closest(".search2").find(".search2-result-overlay").hide();
|
||||
}
|
||||
};
|
||||
let show_overlay = function () {
|
||||
let searchbar = $(this);
|
||||
searchbar.closest(".search2").find(".search2-result-overlay").show();
|
||||
};
|
||||
let searchbar_onclick = function () {
|
||||
let searchbar = $(this);
|
||||
searchbar.val("");
|
||||
searchbar.closest(".search2").find(".search2-result-overlay").show();
|
||||
};
|
||||
let searchbar_typing = function () {
|
||||
let searchbar = $(this);
|
||||
let type = searchbar.data("search-type");
|
||||
let search = $(this).closest(".search2");
|
||||
let overlay_list = search.find(".search2-result-overlay-list");
|
||||
|
||||
if (!searchbar.val().trim() || searchbar.val().trim() === "")
|
||||
return;
|
||||
|
||||
let res_map = new Map();
|
||||
|
||||
let options = {};
|
||||
|
||||
if (type === "member") {
|
||||
options.url = "/api/members/";
|
||||
options.get_data = {
|
||||
"name": searchbar.val()
|
||||
};
|
||||
options.res_handler = function (data) {
|
||||
$.each(data.members, function (index, value) {
|
||||
res_map.set(value.entity_id, value);
|
||||
overlay_list.append("<span tabindex=\"0\" class=\"search2-result-overlay-list-entry\" data-entity-id=\"" + value.entity_id + "\"><li class='list-group-item'><span class=\"badge badge-secondary\">Hinzufügen:</span> " + value.firstname + " " + value.lastname + "</li></span>");
|
||||
});
|
||||
};
|
||||
options.overlay_click_handler = function () {
|
||||
let res = res_map.get($(this).data("entity-id"));
|
||||
setValue(this, res.entity_id, res.firstname + " " + res.lastname);
|
||||
};
|
||||
|
||||
} else if (type === "position") {
|
||||
options.url = "/api/events/units/positions/";
|
||||
options.get_data = {
|
||||
"q": searchbar.val()
|
||||
};
|
||||
options.res_handler = function (data) {
|
||||
$.each(data.positions, function (index, value) {
|
||||
res_map.set(value.entity_id, value);
|
||||
overlay_list.append("<span tabindex=\"0\" class=\"search2-result-overlay-list-entry\" data-entity-id=\"" + value.entity_id + "\"><li class='list-group-item' title='" + value.description + "'><span class=\"badge badge-secondary\">Hinzufügen:</span> " + value.name + "</li></span>");
|
||||
});
|
||||
};
|
||||
options.overlay_click_handler = function () {
|
||||
let res = res_map.get($(this).data("entity-id"));
|
||||
setValue(this, res.entity_id, res.name);
|
||||
};
|
||||
} else {
|
||||
console.error("Unknown search type: " + type);
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: options.url,
|
||||
type: 'GET',
|
||||
data: options.get_data,
|
||||
contentType: 'application/json',
|
||||
success: function (data) {
|
||||
if (is_ok(data)) {
|
||||
overlay_list.empty(); //Remove old overlay list entries
|
||||
show_overlay();
|
||||
|
||||
options.res_handler(data);
|
||||
|
||||
//select search result on click
|
||||
overlay_list.find(".search2-result-overlay-list-entry").off("click").on("click", options.overlay_click_handler);
|
||||
//select search result on enter key press
|
||||
overlay_list.find(".search2-result-overlay-list-entry").off("keyup").on("keyup", function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
options.overlay_click_handler(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
timeout: 3000,
|
||||
error: function () {
|
||||
alert("Verbindung zum Server unterbrochen!");
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
let setValue = function (context, value_id, value) {
|
||||
let search = $(context).closest(".search2");
|
||||
search.find(".search2-selected").val(value).data("value-id", value_id);
|
||||
search.find(".search2-selected-input-group").show();
|
||||
search.find(".search2-search-input-group").hide();
|
||||
hide_overlay(context);
|
||||
search.find(".search2-result-overlay-list").empty();
|
||||
search.find(".search2-searchbar").val("");
|
||||
|
||||
//Fire change event manually on input to detect changes more easily
|
||||
const event = new Event('change');
|
||||
search.find(".search2-selected")[0].dispatchEvent(event);
|
||||
}
|
||||
return {
|
||||
setup
|
||||
}
|
||||
}());
|
|
@ -1,9 +1,11 @@
|
|||
<script src="/js/lib/jquery-3.5.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
|
||||
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="/js/handlebars-v4.7.7.js"></script>
|
||||
<script src="/js/global.js"></script>
|
||||
<script src="/js/searchbar.js"></script>
|
||||
<script src="/js/handlebars-v4.7.7.js"></script>
|
||||
{{#each footer.scripts}}
|
||||
<script src="{{path}}"></script>
|
||||
{{/each}}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
{{> header }}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="wrapper">
|
||||
{{> sidebar }}
|
||||
<div id="content">
|
||||
<h1 style="text-align: center">Einsatzabrechnung {{event.name}}</h1>
|
||||
<div class="nav-progressbar"></div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="card w-100">
|
||||
<div class="card-body eu_approve_summary col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="card w-100">
|
||||
<div class="card-body align-items-center w-100">
|
||||
<h3 style="text-align: center">Abrechnung bestätigen & freigeben</h3>
|
||||
<p class="text-danger" style="text-align: center"><b>Achtung:</b> Hiermit bestätigst du
|
||||
persönlich die sachliche und rechnerische Richtigkeit der Abrechnung.</p>
|
||||
<span class="d-flex justify-content-center"><button
|
||||
class="btn btn-danger approve_btn" role="button">Freigeben</button></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{> footer }}
|
|
@ -0,0 +1,28 @@
|
|||
{{> header }}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="wrapper">
|
||||
{{> sidebar }}
|
||||
<div id="content">
|
||||
<h1 style="text-align: center">Einsatzabrechnung {{event.name}}</h1>
|
||||
<div class="nav-progressbar"></div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="card">
|
||||
<div class="card-body align-items-center">
|
||||
<h3 style="text-align: center">Einsatz schließen</h3>
|
||||
<p class="text-danger"><b>Achtung:</b> Sobald der Einsatz geschlossen wurde können sich
|
||||
keine Helfer mehr eintragen und der Einsatz kann über die Einsatzverwaltung nicht
|
||||
mehr verändert werden. Änderungen sind nur noch über das Abrechnungsmodul möglich!
|
||||
</p>
|
||||
<span class="d-flex justify-content-center"><button
|
||||
class="btn btn-danger close_event_btn" role="button">Einsatz schließen</button></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{> footer }}
|
|
@ -0,0 +1,34 @@
|
|||
{{> header }}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="wrapper">
|
||||
{{> sidebar }}
|
||||
<div id="content">
|
||||
<h1 style="text-align: center">Einsatzabrechnung {{event.name}}</h1>
|
||||
<div class="nav-progressbar"></div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="card w-100">
|
||||
<div class="card-body eu_instances_container row">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="card w-100">
|
||||
<div class="card-body align-items-center">
|
||||
<h3 style="text-align: center">Einsatzzeiten bestätigen</h3>
|
||||
<p class="text-info" style="text-align: center;">Bei allen Einsatzeinheiten ohne realer
|
||||
Beginn oder reales Ende wird die geplante Zeit verwendet.</p>
|
||||
<p class="text-danger" style="text-align: center;"><b>Achtung:</b> Hiermit bestätigst du
|
||||
persönlich, dass die eingetragenen Einsatzzeiten korrekt sind.</p>
|
||||
<span class="d-flex justify-content-center"><button
|
||||
class="btn btn-danger approve_times_btn"
|
||||
role="button">Einsatzzeiten bestätigen</button></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{> footer }}
|
|
@ -0,0 +1,63 @@
|
|||
{{> header }}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="wrapper">
|
||||
{{> sidebar }}
|
||||
<div id="content">
|
||||
<h1 style="text-align: center">Einsatzabrechnung {{event.name}}</h1>
|
||||
<div class="nav-progressbar"></div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Einsatz
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label" for="event_name">Name</label>
|
||||
<div class="col-sm-9">
|
||||
<input class="form-control qsf" disabled id="event_name" readonly
|
||||
type="text" value="{{event.name}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label" for="event_start">Anfang</label>
|
||||
<div class="col-sm-9">
|
||||
<input class="form-control qsf" disabled id="event_start" readonly
|
||||
type="datetime-local" value="{{event.start}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label" for="event_end">Ende</label>
|
||||
<div class="col-sm-9">
|
||||
<input class="form-control qsf" disabled id="event_end" readonly
|
||||
type="datetime-local" value="{{event.end}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Aktionen
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-center w-100 action_btn">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{> footer }}
|
|
@ -0,0 +1,30 @@
|
|||
{{> header }}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="wrapper">
|
||||
{{> sidebar }}
|
||||
<div id="content">
|
||||
<h1 style="text-align: center">Einsatzabrechnung {{event.name}}</h1>
|
||||
<div class="nav-progressbar"></div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="card w-100">
|
||||
<div class="card-body eu_instances_container row">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="card w-100">
|
||||
<div class="card-body align-items-center">
|
||||
<h3 style="text-align: center">Einsatzabrechnung speichern & überprüfen</h3>
|
||||
<span class="d-flex justify-content-center"><button
|
||||
class="btn btn-warning check_personnel_billing_btn"
|
||||
role="button">Abrechnung überprüfen</button></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{> footer }}
|
|
@ -57,15 +57,24 @@
|
|||
|
||||
<ul class="nav nav-tabs edit_event_cast_navtabs" id="edit_event_cast_tab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" href="#edit_event_cast_core_data_tabpanel" id="core-tab" data-toggle="tab" data-bs-toggle="tab" data-bs-target="#edit_event_cast_core_data_tabpanel" aria-controls="core-tab" aria-selected="true">Basisdaten</a>
|
||||
<a class="nav-link active" href="#edit_event_cast_core_data_tabpanel" id="core-tab"
|
||||
data-toggle="tab" data-bs-toggle="tab"
|
||||
data-bs-target="#edit_event_cast_core_data_tabpanel" aria-controls="core-tab"
|
||||
aria-selected="true">Basisdaten</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="#edit_event_cast_cast_tabpanel" id="cast-tab" data-toggle="tab" data-bs-toggle="tab" data-bs-target="#edit_event_cast_cast_tabpanel" aria-controls="cast-tab" aria-selected="true">Besetzung</a>
|
||||
<a class="nav-link" href="#edit_event_cast_cast_tabpanel" id="cast-tab" data-toggle="tab"
|
||||
data-bs-toggle="tab" data-bs-target="#edit_event_cast_cast_tabpanel"
|
||||
aria-controls="cast-tab" aria-selected="true">Besetzung</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="#" id="edit_event_delete_link">Löschen</a>
|
||||
</li>
|
||||
</ul><br>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" href="/portal/eb/event?event_id={{event_id}}" id="billing_link">Einsatzabrechnung</a>
|
||||
</li>
|
||||
</ul>
|
||||
<br>
|
||||
<div class="tab-content" id="edit_event_cast_tab_content">
|
||||
<div class="tab-pane fade show active edit_event_core_data" role="tabpanel" aria-labelledby="core-tab" id="edit_event_cast_core_data_tabpanel"></div>
|
||||
<div class="tab-pane fade" role="tabpanel" aria-labelledby="cast-tab" id="edit_event_cast_cast_tabpanel">
|
||||
|
|
|
@ -1 +1 @@
|
|||
v0.2-80-gf8eca4a
|
||||
v0.2-96-g136781c
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
use std::convert::TryFrom;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use diesel::{ExpressionMethods, PgTextExpressionMethods, QueryDsl, sql_query};
|
||||
use diesel::pg::types::sql_types::Array;
|
||||
use diesel::sql_types::BigInt;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use crate::database::controller::groups::get_groups_for_member;
|
||||
use crate::database::controller::members::TempQuery;
|
||||
use crate::database::model::api_members::RawMemberSearchResult;
|
||||
use crate::diesel::RunQueryDsl;
|
||||
use crate::helper::check_access::check_access_to_member_or_group;
|
||||
use crate::helper::order_enum::Order;
|
||||
use crate::helper::permission::PermissionMatrix;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::modules::api::members::get_member::MemberSearchResult;
|
||||
use diesel::{ExpressionMethods, PgTextExpressionMethods, QueryDsl,sql_query};
|
||||
use rocket::State;
|
||||
use chrono::NaiveDate;
|
||||
use crate::helper::permission::PermissionMatrix;
|
||||
use diesel::pg::types::sql_types::Array;
|
||||
use crate::modules::member_management::model::groups::Group;
|
||||
use crate::schema::members;
|
||||
|
||||
pub fn get_raw_member_search_result(
|
||||
settings: &State<Settings>,
|
||||
|
@ -125,13 +133,6 @@ pub fn get_member_search_result_by_name(
|
|||
Ok(members)
|
||||
}
|
||||
|
||||
use crate::schema::members;
|
||||
use diesel::sql_types::BigInt;
|
||||
use crate::database::controller::members::TempQuery;
|
||||
use crate::helper::order_enum::Order;
|
||||
use std::convert::TryFrom;
|
||||
use crate::modules::member_management::model::groups::Group;
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize, QueryableByName)]
|
||||
#[table_name = "members"]
|
||||
pub struct MemberListEntry{
|
||||
|
@ -300,8 +301,7 @@ pub fn get_member_list_count(settings: &State<Settings>, groups: Option<Vec<uuid
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// DO NOT USE! Will be removed soon, use crate::database::controller::entities::remove_entity instead!
|
||||
#[deprecated = "use crate::database::controller::entities::remove_entity instead"]
|
||||
pub fn delete_entity_deprecated(
|
||||
settings: &State<Settings>,
|
||||
entity_id_to_delete: uuid::Uuid,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
pub mod states;
|
||||
pub mod personnel_billing_rates;
|
||||
pub mod personnel_billing;
|
|
@ -0,0 +1,58 @@
|
|||
use bigdecimal::BigDecimal;
|
||||
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use crate::schema::personnel_billing;
|
||||
use crate::Settings;
|
||||
|
||||
#[derive(Queryable, Clone, AsChangeset, Insertable)]
|
||||
#[table_name = "personnel_billing"]
|
||||
#[changeset_options(treat_none_as_null = "true")]
|
||||
#[primary_key(position_instance_id)]
|
||||
pub struct RawPersonnelBilling {
|
||||
pub(crate) position_instance_id: uuid::Uuid,
|
||||
pub(crate) member_id: uuid::Uuid,
|
||||
pub(crate) fulfilled_time: i32,
|
||||
pub(crate) money_for_time: BigDecimal,
|
||||
pub(crate) money_from_lump_sum: BigDecimal,
|
||||
pub(crate) total_money: BigDecimal,
|
||||
}
|
||||
|
||||
pub fn remove(settings: &State<Settings>, position_instance_id: uuid::Uuid) -> Result<usize, diesel::result::Error> {
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match diesel::delete(personnel_billing::table).filter(personnel_billing::dsl::position_instance_id.eq(position_instance_id)).execute(&connection) {
|
||||
Ok(num) => Ok(num),
|
||||
Err(e) => {
|
||||
error!("Couldn't remove personnel billing entry: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(settings: &State<Settings>, data: RawPersonnelBilling) -> Result<(), diesel::result::Error> {
|
||||
//Delete existing entry if exists
|
||||
remove(settings, data.position_instance_id);
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
match diesel::insert_into(personnel_billing::table).values(data).execute(&connection) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Couldn't insert personnel billing entry: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(settings: &State<Settings>, position_instance_id: uuid::Uuid) -> Result<Option<RawPersonnelBilling>, diesel::result::Error> {
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match personnel_billing::table.filter(personnel_billing::dsl::position_instance_id.eq(position_instance_id)).get_result(&connection).optional() {
|
||||
Ok(res) => Ok(res),
|
||||
Err(e) => {
|
||||
error!("Couldn't get personnel billing details: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
use bigdecimal::BigDecimal;
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use crate::Settings;
|
||||
|
||||
#[derive(Queryable, Clone)]
|
||||
pub struct RawBillingRate {
|
||||
pub(crate) billing_rate_id: uuid::Uuid,
|
||||
pub(crate) name: String,
|
||||
pub(crate) description: Option<String>,
|
||||
pub(crate) active: bool,
|
||||
pub(crate) payment_per_hour: BigDecimal,
|
||||
pub(crate) lump_sum: BigDecimal,
|
||||
}
|
||||
|
||||
pub fn get_billing_rates(settings: &State<Settings>) -> Result<Vec<RawBillingRate>, diesel::result::Error> {
|
||||
use crate::schema::personnel_billing_rates::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match personnel_billing_rates.load(&connection) {
|
||||
Ok(rates) => Ok(rates),
|
||||
Err(e) => {
|
||||
error!("Couldn't retrieve billing rates: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_billing_rate(settings: &State<Settings>, billing_rate_id: uuid::Uuid) -> Result<RawBillingRate, diesel::result::Error> {
|
||||
use crate::schema::personnel_billing_rates;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match personnel_billing_rates::table.filter(personnel_billing_rates::dsl::billing_rate_id.eq(billing_rate_id)).get_result(&connection) {
|
||||
Ok(rate) => Ok(rate),
|
||||
Err(e) => {
|
||||
error!("Couldn't retrieve billing rate: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, sql_query};
|
||||
use diesel::pg::types::sql_types::Uuid;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use crate::Settings;
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize)]
|
||||
pub struct BillingState {
|
||||
entity_id: uuid::Uuid,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
pub(crate) final_approve: bool,
|
||||
}
|
||||
|
||||
pub fn get_billing_states(settings: &State<Settings>) -> Result<Vec<BillingState>, diesel::result::Error> {
|
||||
use crate::schema::billing_states::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match billing_states.order(order.asc()).select((entity_id, name, description, final_approve)).get_results(&connection) {
|
||||
Ok(bs) => Ok(bs),
|
||||
Err(e) => {
|
||||
error!("Couldn't get billing states: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_billing_state(settings: &State<Settings>, state: uuid::Uuid) -> Result<BillingState, diesel::result::Error> {
|
||||
use crate::schema::billing_states::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match billing_states.select((entity_id, name, description, final_approve)).filter(entity_id.eq(state)).get_result(&connection) {
|
||||
Ok(bs) => Ok(bs),
|
||||
Err(e) => {
|
||||
error!("Couldn't get billing state: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_billing_states_for_event(settings: &State<Settings>, event: uuid::Uuid) -> Result<Vec<uuid::Uuid>, diesel::result::Error> {
|
||||
use crate::schema::eu_instances::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match eu_instances.filter(event_id.eq(event)).filter(billing_state_id.is_not_null()).select(billing_state_id).get_results(&connection) {
|
||||
Ok(states) => Ok(states.iter().map(|state: &Option<uuid::Uuid>| state.clone().unwrap()).collect()),
|
||||
Err(e) => {
|
||||
error!("Couldn't get billing states from instance for event: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(QueryableByName)]
|
||||
struct TempQuery {
|
||||
#[sql_type = "Uuid"]
|
||||
pub(crate) billing_state_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
pub fn get_min_billing_states_for_event(settings: &State<Settings>, event: uuid::Uuid) -> Result<Option<uuid::Uuid>, diesel::result::Error> {
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
let res: Result<Option<TempQuery>, diesel::result::Error> = sql_query("SELECT billing_state_id FROM eu_instances AS eui INNER JOIN billing_states bs on eui.billing_state_id = bs.entity_id WHERE event_id = $1 ORDER BY bs.order ASC LIMIT 1;").bind::<crate::diesel::sql_types::Uuid, _>(event).get_result(&connection);
|
||||
match res {
|
||||
Ok(res) => {
|
||||
match res {
|
||||
Some(res) => Ok(Some(res.billing_state_id)),
|
||||
None => Ok(None),
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Couldn't get min billing states from instance for event: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,15 @@
|
|||
use crate::helper::settings::Settings;
|
||||
use diesel::{Connection, PgConnection};
|
||||
use rocket::State;
|
||||
|
||||
use crate::helper::settings::Settings;
|
||||
|
||||
pub fn establish_connection(settings: &State<Settings>) -> PgConnection {
|
||||
//TODO: return Error
|
||||
let db_url = settings.database.connection_string.clone();
|
||||
PgConnection::establish(&db_url).expect(&format!("Error connecting to database."))
|
||||
match PgConnection::establish(&db_url) {
|
||||
Ok(con) => con,
|
||||
Err(e) => {
|
||||
panic!("Couldn't connect to database: {}. Database offline?", e);
|
||||
//TODO: Return Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,16 +5,20 @@ use diesel::pg::types::sql_types::Uuid;
|
|||
use rocket::State;
|
||||
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use crate::database::controller::events::instances::instance_positions::RawPositionInstance;
|
||||
use crate::database::controller::events::instances::instances::RawEventUnitInstance;
|
||||
use crate::database::controller::events::templates::vehicle_positions::get_eu_vehicle_positions_for_template;
|
||||
use crate::database::model::events::{Event, EventType, EventUnitInstancePosition, EventUnitInstanceVehiclePosition, EventUnitPosition, EventUnitTemplate, EventUnitVehiclePosition};
|
||||
use crate::diesel::QueryDsl;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::modules::api::events::instances::read::PositionInstance;
|
||||
use crate::schema::eu_position_instances::dsl::eu_position_instances;
|
||||
use crate::schema::eu_positions_templates;
|
||||
|
||||
pub mod templates;
|
||||
pub mod instances;
|
||||
//TODO: migrate to multiple files to improve readability
|
||||
//TODO: document this whole file...
|
||||
|
||||
pub fn add_event(settings: &State<Settings>, data: Event) -> Result<Event, diesel::result::Error> {
|
||||
use crate::schema::events::dsl::*;
|
||||
|
@ -30,12 +34,93 @@ pub fn add_event(settings: &State<Settings>, data: Event) -> Result<Event, diese
|
|||
}
|
||||
}
|
||||
|
||||
pub fn change_event(settings: &State<Settings>, data: Event) -> Result<Event, diesel::result::Error>{
|
||||
/// Sets state for event from 0 or 2 to 4 and therefore locks the event.
|
||||
/// Used in API endpoint for billing module
|
||||
/// Returns an empty Ok result or an [diesel::result::Error].
|
||||
pub fn close_event(settings: &State<Settings>, event_id: uuid::Uuid) -> Result<(), diesel::result::Error> {
|
||||
use crate::schema::events::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match diesel::update(events.filter(entity_id.eq(data.entity_id))).set(data).get_result(&connection){
|
||||
match diesel::update(events.filter(entity_id.eq(event_id)).filter(state.eq(any(vec!(0, 2))))).set(state.eq(4)).execute(&connection) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Couldn't close event: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets state for event from 4 to 6
|
||||
/// Used in API endpoint for billing module
|
||||
/// Returns an empty Ok result or an [diesel::result::Error].
|
||||
pub fn approve_event_times(settings: &State<Settings>, event_id: uuid::Uuid) -> Result<(), diesel::result::Error> {
|
||||
use crate::schema::events::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match diesel::update(events.filter(entity_id.eq(event_id)).filter(state.eq(4))).set(state.eq(6)).execute(&connection) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Couldn't approve event times: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets state for event from 6 to 7
|
||||
/// Used in API endpoint for billing module
|
||||
/// Returns an empty Ok result or an [diesel::result::Error].
|
||||
pub fn finish_personnel_billing(settings: &State<Settings>, event_id: uuid::Uuid) -> Result<(), diesel::result::Error> {
|
||||
use crate::schema::events::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match diesel::update(events.filter(entity_id.eq(event_id)).filter(state.eq(6))).set(state.eq(7)).execute(&connection) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Couldn't finish personnel billing: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn approve_stage(settings: &State<Settings>, event_id2: uuid::Uuid, stage: uuid::Uuid, caller: uuid::Uuid) -> Result<(), diesel::result::Error> {
|
||||
use crate::schema::eu_instances::dsl::*;
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match diesel::update(eu_instances).filter(event_id.eq(event_id2)).set((billing_state_id.eq(stage), billing_state_author.eq(caller))).execute(&connection) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Couldn't approve stage: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets state for event from 7 to 8
|
||||
/// Used in API endpoint for billing module
|
||||
/// Returns an empty Ok result or an [diesel::result::Error].
|
||||
pub fn finish_billing(settings: &State<Settings>, event_id: uuid::Uuid) -> Result<(), diesel::result::Error> {
|
||||
use crate::schema::events::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match diesel::update(events.filter(entity_id.eq(event_id)).filter(state.eq(7))).set(state.eq(8)).execute(&connection) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Couldn't finish billing: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_event(settings: &State<Settings>, data: Event) -> Result<Event, diesel::result::Error> {
|
||||
use crate::schema::events::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match diesel::update(events.filter(entity_id.eq(data.entity_id))).set(data).get_result(&connection) {
|
||||
Ok(org) => Ok(org),
|
||||
Err(e) => {
|
||||
error!("Couldn't update event: {}", e);
|
||||
|
@ -424,8 +509,8 @@ pub fn add_position_instances_for_instance(settings: &State<Settings>, instance_
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_instance_positions(settings: &State<Settings>, instance_id2: uuid::Uuid) -> Result<Vec<EventUnitInstancePosition>, diesel::result::Error>{
|
||||
|
||||
#[deprecated]
|
||||
pub fn get_instance_positions_name_description(settings: &State<Settings>, instance_id2: uuid::Uuid) -> Result<Vec<EventUnitInstancePosition>, diesel::result::Error> {
|
||||
let connection = establish_connection(settings);
|
||||
let position_instances: Result<Vec<EventUnitInstancePosition>, diesel::result::Error> = sql_query(
|
||||
"SELECT position_instance_id, instance_id, position_id, taken_by, name, description, requirements FROM eu_position_instances INNER JOIN eu_positions ON position_id = entity_id WHERE instance_id = $1;",
|
||||
|
@ -433,7 +518,7 @@ pub fn get_instance_positions(settings: &State<Settings>, instance_id2: uuid::Uu
|
|||
.bind::<Uuid, _>(instance_id2)
|
||||
.get_results(&connection);
|
||||
|
||||
match position_instances{
|
||||
match position_instances {
|
||||
Ok(pos) => Ok(pos),
|
||||
Err(e) => {
|
||||
error!("Couldn't get instance positions: {}", e);
|
||||
|
@ -442,8 +527,31 @@ pub fn get_instance_positions(settings: &State<Settings>, instance_id2: uuid::Uu
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_instance_vehicle_positions(settings: &State<Settings>, instance_id2: uuid::Uuid) -> Result<Vec<EventUnitInstanceVehiclePosition>, diesel::result::Error>{
|
||||
pub fn get_position_instances(settings: &State<Settings>, instance_id: uuid::Uuid) -> Result<Vec<RawPositionInstance>, diesel::result::Error> {
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match eu_position_instances.filter(crate::schema::eu_position_instances::dsl::instance_id.eq(instance_id)).get_results(&connection) {
|
||||
Ok(pos) => Ok(pos),
|
||||
Err(e) => {
|
||||
error!("Couldn't get instance positions: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_position_instance(settings: &State<Settings>, position_instance_id: uuid::Uuid) -> Result<RawPositionInstance, diesel::result::Error> {
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match eu_position_instances.filter(crate::schema::eu_position_instances::dsl::position_instance_id.eq(position_instance_id)).get_result(&connection) {
|
||||
Ok(pos) => Ok(pos),
|
||||
Err(e) => {
|
||||
error!("Couldn't get position instance: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_instance_vehicle_positions(settings: &State<Settings>, instance_id2: uuid::Uuid) -> Result<Vec<EventUnitInstanceVehiclePosition>, diesel::result::Error> {
|
||||
let connection = establish_connection(settings);
|
||||
let position_instances: Result<Vec<EventUnitInstanceVehiclePosition>, diesel::result::Error> = sql_query(
|
||||
"SELECT instance_id, position_id, taken_by, name, description, required_vehicle_category FROM eu_position_instances INNER JOIN eu_vehicle_positions ON position_id = entity_id WHERE instance_id = $1;",
|
||||
|
@ -451,7 +559,7 @@ pub fn get_instance_vehicle_positions(settings: &State<Settings>, instance_id2:
|
|||
.bind::<Uuid, _>(instance_id2)
|
||||
.get_results(&connection);
|
||||
|
||||
match position_instances{
|
||||
match position_instances {
|
||||
Ok(pos) => Ok(pos),
|
||||
Err(e) => {
|
||||
error!("Couldn't get instance vehicle positions: {}", e);
|
||||
|
@ -494,7 +602,7 @@ pub fn get_eu_position(settings: &State<Settings>, position_id: uuid::Uuid) -> R
|
|||
match eu_positions.filter(entity_id.eq(position_id)).get_result(&connection){
|
||||
Ok(pos) => Ok(pos),
|
||||
Err(e) => {
|
||||
error!("Couldn't get eu_position: {}", e);
|
||||
error!("Couldn't get eu_position {}: {}", position_id, e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
use chrono::Utc;
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, sql_query};
|
||||
use diesel::sql_types::{Jsonb, Nullable, SmallInt, Text, Timestamp, Uuid};
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use crate::schema::eu_position_instances;
|
||||
use crate::Settings;
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize, AsChangeset, Insertable)]
|
||||
#[table_name = "eu_position_instances"]
|
||||
#[changeset_options(treat_none_as_null = "true")]
|
||||
#[primary_key(position_instance_id)]
|
||||
pub struct RawPositionInstance {
|
||||
pub(crate) instance_id: uuid::Uuid,
|
||||
pub(crate) position_id: uuid::Uuid,
|
||||
pub(crate) taken_by: Option<uuid::Uuid>,
|
||||
pub(crate) position_instance_id: uuid::Uuid,
|
||||
pub(crate) real_start_time: Option<chrono::DateTime<Utc>>,
|
||||
pub(crate) real_end_time: Option<chrono::DateTime<Utc>>,
|
||||
pub(crate) billable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize, AsChangeset, Insertable)]
|
||||
#[table_name = "eu_position_instances"]
|
||||
#[changeset_options(treat_none_as_null = "true")]
|
||||
#[primary_key(position_instance_id)]
|
||||
pub struct RawPositionInstanceInsert {
|
||||
pub(crate) instance_id: uuid::Uuid,
|
||||
pub(crate) position_id: uuid::Uuid,
|
||||
pub(crate) taken_by: Option<uuid::Uuid>,
|
||||
pub(crate) position_instance_id: Option<uuid::Uuid>,
|
||||
pub(crate) real_start_time: Option<chrono::DateTime<Utc>>,
|
||||
pub(crate) real_end_time: Option<chrono::DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub fn create(settings: &State<Settings>, data: RawPositionInstanceInsert) -> Result<RawPositionInstance, diesel::result::Error> {
|
||||
let connection = establish_connection(settings);
|
||||
use crate::schema::eu_position_instances::dsl::eu_position_instances;
|
||||
|
||||
match diesel::insert_into(eu_position_instances).values(data).get_result::<RawPositionInstance>(&connection) {
|
||||
Ok(res) => Ok(res),
|
||||
Err(e) => {
|
||||
error!("Couldn't insert new PositionInstance: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(settings: &State<Settings>, position_instance_id: uuid::Uuid) -> Result<(), diesel::result::Error> {
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
use crate::schema::eu_position_instances::dsl::eu_position_instances;
|
||||
match diesel::delete(eu_position_instances).filter(crate::schema::eu_position_instances::dsl::position_instance_id.eq(position_instance_id)).execute(&connection) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Couldn't delete position_instance {}: {}", position_instance_id, e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize, AsChangeset, Insertable)]
|
||||
#[table_name = "eu_position_instances"]
|
||||
#[primary_key(position_instance_id)]
|
||||
pub struct RawPositionInstanceChangeset {
|
||||
pub(crate) instance_id: Option<uuid::Uuid>,
|
||||
pub(crate) position_id: Option<uuid::Uuid>,
|
||||
pub(crate) taken_by: Option<Option<uuid::Uuid>>,
|
||||
pub(crate) position_instance_id: uuid::Uuid,
|
||||
pub(crate) real_start_time: Option<Option<chrono::DateTime<Utc>>>,
|
||||
pub(crate) real_end_time: Option<Option<chrono::DateTime<Utc>>>,
|
||||
pub(crate) billable: Option<Option<bool>>
|
||||
}
|
||||
|
||||
pub fn update(settings: &State<Settings>, patch: RawPositionInstanceChangeset) -> Result<RawPositionInstance, diesel::result::Error> {
|
||||
use crate::schema::eu_position_instances::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
match diesel::update(eu_position_instances.filter(position_instance_id.eq(patch.position_instance_id))).set(patch).get_result(&connection) {
|
||||
Ok(update) => Ok(update),
|
||||
Err(e) => {
|
||||
error!("Couldn't patch position instance: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces null values for real_start_time and real_end_time in eu_position_instances to the real times their related instance
|
||||
/// *Warning: Should be run after [crate::database::controller::events::instances::instances::set_missing_real_times_to_planned_times]*
|
||||
pub fn set_missing_real_times_to_instance_times(settings: &State<Settings>, event: uuid::Uuid) -> Result<(), diesel::result::Error> {
|
||||
use diesel::sql_query;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
match sql_query("UPDATE eu_position_instances AS epi SET real_start_time = ei.real_start_time FROM eu_instances AS ei WHERE epi.real_start_time IS NULL AND ei.instance_id = epi.instance_id AND ei.event_id = $1;").bind::<diesel::sql_types::Uuid, _>(event).execute(&connection) {
|
||||
Ok(res) => (),
|
||||
Err(e) => {
|
||||
error!("Couldn't set missing real times to planned times for position_instances: {}", e);
|
||||
return Err(e)
|
||||
}
|
||||
}
|
||||
match sql_query("UPDATE eu_position_instances AS epi SET real_end_time = ei.real_end_time FROM eu_instances AS ei WHERE epi.real_end_time IS NULL AND ei.instance_id = epi.instance_id AND ei.event_id = $1;").bind::<diesel::sql_types::Uuid, _>(event).execute(&connection) {
|
||||
Ok(res) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Couldn't set missing real times to planned times for position_instances: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,8 @@ pub struct RawEventUnitInstance {
|
|||
pub(crate) real_start_time: Option<chrono::DateTime<Utc>>,
|
||||
pub(crate) real_end_time: Option<chrono::DateTime<Utc>>,
|
||||
pub(crate) billing_rate_id: Option<uuid::Uuid>,
|
||||
pub(crate) billing_state_id: Option<String>,
|
||||
pub(crate) billing_state_id: Option<uuid::Uuid>,
|
||||
pub(crate) billing_state_author: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
pub fn get_instances(settings: &State<Settings>, event: uuid::Uuid) -> Result<Vec<RawEventUnitInstance>, diesel::result::Error> {
|
||||
|
@ -36,6 +37,20 @@ pub fn get_instances(settings: &State<Settings>, event: uuid::Uuid) -> Result<Ve
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_instance(settings: &State<Settings>, instance: uuid::Uuid) -> Result<RawEventUnitInstance, diesel::result::Error> {
|
||||
use crate::schema::eu_instances::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
match eu_instances.filter(instance_id.eq(instance)).get_result(&connection) {
|
||||
Ok(instances) => Ok(instances),
|
||||
Err(e) => {
|
||||
error!("Couldn't get event unit instance: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize, AsChangeset, Insertable)]
|
||||
#[table_name = "eu_instances"]
|
||||
#[primary_key(entity_id)]
|
||||
|
@ -48,7 +63,7 @@ pub struct RawEventUnitInstanceChangeset {
|
|||
pub(crate) real_start_time: Option<Option<chrono::DateTime<Utc>>>,
|
||||
pub(crate) real_end_time: Option<Option<chrono::DateTime<Utc>>>,
|
||||
pub(crate) billing_rate_id: Option<Option<uuid::Uuid>>,
|
||||
pub(crate) billing_state_id: Option<Option<String>>,
|
||||
pub(crate) billing_state_id: Option<Option<uuid::Uuid>>,
|
||||
}
|
||||
|
||||
pub fn update_instance(settings: &State<Settings>, patch: RawEventUnitInstanceChangeset) -> Result<RawEventUnitInstance, diesel::result::Error> {
|
||||
|
@ -62,4 +77,25 @@ pub fn update_instance(settings: &State<Settings>, patch: RawEventUnitInstanceCh
|
|||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces null values for real_start_time and real_end_time in eu_instances to the planned_start_time/planned_end_time
|
||||
pub fn set_missing_real_times_to_planned_times(settings: &State<Settings>, event: uuid::Uuid) -> Result<(), diesel::result::Error> {
|
||||
use diesel::sql_query;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
match sql_query("UPDATE eu_instances SET real_start_time = planned_start_time WHERE instance_id IN (SELECT instance_id FROM eu_instances WHERE event_id = $1 AND real_start_time IS NULL);").bind::<diesel::sql_types::Uuid, _>(event).execute(&connection) {
|
||||
Ok(res) => (),
|
||||
Err(e) => {
|
||||
error!("Couldn't set missing real times to planned times for instances: {}", e);
|
||||
return Err(e)
|
||||
}
|
||||
}
|
||||
match sql_query("UPDATE eu_instances SET real_end_time = planned_end_time WHERE instance_id IN (SELECT instance_id FROM eu_instances WHERE event_id = $1 AND real_end_time IS NULL);").bind::<diesel::sql_types::Uuid, _>(event).execute(&connection) {
|
||||
Ok(res) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Couldn't set missing real times to planned times for instances: {}", e);
|
||||
return Err(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
use diesel::{JoinOnDsl, QueryDsl, sql_query};
|
||||
use diesel::pg::expression::array_comparison::any;
|
||||
use diesel::pg::types::sql_types::{Array, Uuid};
|
||||
use diesel::sql_types::{BigInt, Text};
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use crate::database::controller::groups::get_groups_for_member;
|
||||
use crate::database::model::members::RawMember;
|
||||
use crate::diesel::prelude::*;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::modules::member_management::model::member::Member;
|
||||
use diesel::pg::expression::array_comparison::any;
|
||||
use diesel::pg::types::sql_types::{Uuid, Array};
|
||||
use diesel::sql_types::{BigInt, Text};
|
||||
use diesel::{sql_query, JoinOnDsl, QueryDsl};
|
||||
use rocket::State;
|
||||
use crate::schema::groups_entities::dsl::groups_entities;
|
||||
|
||||
/*impl<DB> FromSql<SmallInt, DB> for Sex
|
||||
|
@ -240,6 +241,7 @@ pub fn get_members_by_user_uuid(uuid: uuid::Uuid, settings: &State<Settings>) ->
|
|||
/// * 'settings' - Settings, as managed State
|
||||
/// #Returns
|
||||
/// * 'Member'
|
||||
//TODO: return Result not OPtion
|
||||
pub fn get_member_by_uuid(uuid: uuid::Uuid, settings: &State<Settings>) -> Option<Member> {
|
||||
let raw_member: RawMember = get_raw_member_by_uuid(uuid, &settings)?;
|
||||
|
||||
|
|
|
@ -24,4 +24,5 @@ pub mod entities;
|
|||
pub mod organisers;
|
||||
pub mod events;
|
||||
pub mod event_requests;
|
||||
pub mod permissions;
|
||||
pub mod permissions;
|
||||
pub mod billing;
|
|
@ -0,0 +1,14 @@
|
|||
use lettre::{AsyncSmtpTransport, Message, Tokio1Executor};
|
||||
use lettre::message::MessageBuilder;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use rocket::futures::SinkExt;
|
||||
use rocket::State;
|
||||
|
||||
use crate::Settings;
|
||||
|
||||
pub fn setup(settings: &Settings) -> Result<AsyncSmtpTransport<Tokio1Executor>, lettre::transport::smtp::Error> {
|
||||
let creds = Credentials::new(settings.mail.smtp_username.clone(), settings.mail.smtp_password.clone());
|
||||
let relay = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&settings.mail.smtp_host.clone())?;
|
||||
Ok(relay.credentials(creds).build())
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod queue;
|
||||
pub mod worker;
|
||||
pub mod worker;
|
||||
pub mod mailer;
|
|
@ -1,55 +1,74 @@
|
|||
use std::sync::RwLock;
|
||||
use std::collections::VecDeque;
|
||||
use chrono::NaiveDateTime;
|
||||
use std::io::{Write, Read};
|
||||
use std::io::{Read, Write};
|
||||
use std::ops::Add;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
|
||||
use lettre::Message;
|
||||
|
||||
use crate::helper::mail_queue::worker::send_mail;
|
||||
use crate::Settings;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct MailQueue{
|
||||
pub(crate) mails: RwLock<VecDeque<Mail>>
|
||||
pub struct MessageWatcher {
|
||||
message: Message,
|
||||
tries: i8,
|
||||
next_try_after: Option<chrono::DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct Mail{
|
||||
pub(crate) uuid: uuid::Uuid,
|
||||
pub(crate) from: String,
|
||||
pub(crate) to: Vec<String>,
|
||||
pub(crate) subject: String,
|
||||
pub(crate) cc: Vec<String>,
|
||||
pub(crate) bcc: Vec<String>,
|
||||
pub(crate) reply_to: Option<String>,
|
||||
pub(crate) body: String,
|
||||
pub(crate) deliver_until: Option<NaiveDateTime>
|
||||
}
|
||||
|
||||
impl MailQueue{
|
||||
pub fn add_mail(&self, mail: Mail) -> Result<(), std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<Mail>>>>{
|
||||
self.mails.write()?.push_back(mail);
|
||||
match self.save_to_disk(){
|
||||
Ok(_) => {}
|
||||
Err(e) => error!("Couldn't save mail_queue to disk: {}", e)
|
||||
impl From<Message> for MessageWatcher {
|
||||
fn from(msg: Message) -> Self {
|
||||
MessageWatcher {
|
||||
message: msg,
|
||||
tries: 0,
|
||||
next_try_after: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MailQueue {
|
||||
pub(crate) mails: RwLock<VecDeque<MessageWatcher>>,
|
||||
}
|
||||
|
||||
impl MailQueue {
|
||||
pub fn add_mail(&self, mail: Message) -> Result<(), std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<MessageWatcher>>>> {
|
||||
self.mails.write()?.push_back(mail.into());
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_next(&self) -> Result<Option<Mail>, std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<Mail>>>>{
|
||||
// TODO: Check if mail expired (deliver_until check)
|
||||
pub fn add_msg_watcher(&self, msg_watcher: MessageWatcher) -> Result<(), std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<MessageWatcher>>>> {
|
||||
self.mails.write()?.push_back(msg_watcher);
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_next(&self) -> Result<Option<MessageWatcher>, std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<MessageWatcher>>>> {
|
||||
Ok(self.mails.write()?.pop_front())
|
||||
}
|
||||
pub fn process_next(&self) -> Result<(), std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<Mail>>>>{
|
||||
let mail = self.get_next()?;
|
||||
match mail{
|
||||
Some(mail) => {
|
||||
match send_mail(mail.clone()){
|
||||
Ok(_) => {
|
||||
match self.save_to_disk(){
|
||||
Ok(_) => {}
|
||||
Err(e) => error!("Couldn't save mail_queue to disk: {}", e)
|
||||
}
|
||||
},
|
||||
pub async fn process_next(&self, settings: &Settings) -> Result<(), std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<MessageWatcher>>>> {
|
||||
let mail_watcher = self.get_next()?;
|
||||
|
||||
match mail_watcher {
|
||||
Some(mut mail_watcher) => {
|
||||
if let Some(next_try_after) = mail_watcher.next_try_after {
|
||||
let current = Utc::now();
|
||||
if next_try_after.ge(¤t) { //Not due
|
||||
self.add_msg_watcher(mail_watcher)?;
|
||||
return Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
match send_mail(mail_watcher.message.clone(), settings).await {
|
||||
Ok(_) => {},
|
||||
Err(_) => {
|
||||
//TODO: remove emails after x attempts
|
||||
return self.add_mail(mail)
|
||||
//Do not re-add mail after third try:
|
||||
if mail_watcher.tries < 3 {
|
||||
if mail_watcher.tries == 0 {
|
||||
mail_watcher.next_try_after = Some(Utc::now().add(Duration::minutes(1))); //First retry after 1 minute
|
||||
} else if mail_watcher.tries == 1 {
|
||||
mail_watcher.next_try_after = Some(Utc::now().add(Duration::minutes(10))); //Second retry after 10 minutes
|
||||
} else if mail_watcher.tries == 2 {
|
||||
mail_watcher.next_try_after = Some(Utc::now().add(Duration::minutes(600))); //Third retry after 10 hours
|
||||
}
|
||||
mail_watcher.tries = mail_watcher.tries + 1;
|
||||
self.add_msg_watcher(mail_watcher)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -58,7 +77,7 @@ impl MailQueue{
|
|||
}
|
||||
}
|
||||
pub fn load_or_create_new() -> MailQueue {
|
||||
match std::fs::File::open("mail_queue.txt"){
|
||||
/*match std::fs::File::open("mail_queue.txt"){
|
||||
Ok(mut file) => {
|
||||
let mut contents = String::new();
|
||||
match file.read_to_string(&mut contents){
|
||||
|
@ -76,33 +95,16 @@ impl MailQueue{
|
|||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Couldn't read mail queue from disk: {}. Creating new empty queue.", e);
|
||||
MailQueue{
|
||||
mails: RwLock::new(VecDeque::new())
|
||||
}
|
||||
warn!("Couldn't read mail queue from disk: {}. Creating new empty queue.", e);*/
|
||||
MailQueue {
|
||||
mails: RwLock::new(VecDeque::new())
|
||||
}/*
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}/*
|
||||
fn save_to_disk(&self) -> std::io::Result<()> {
|
||||
let serialized = serde_json::to_string(&self)?;
|
||||
let mut file = std::fs::File::create("mail_queue.txt")?;
|
||||
file.write_all(serialized.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mail{
|
||||
/// Create Mail with from, to, subject, cc, bcc, reply_to, deliver_until
|
||||
pub(crate) fn new(from: String, to: Vec<String>, subject: String, cc: Vec<String>, bcc: Vec<String>, reply_to: Option<String>, body: String, deliver_until: Option<NaiveDateTime>) -> Mail {
|
||||
Mail{
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
cc,
|
||||
bcc,
|
||||
reply_to,
|
||||
body,
|
||||
deliver_until
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
|
@ -1,84 +1,23 @@
|
|||
use crate::helper::mail_queue::queue::Mail;
|
||||
use std::process::Command;
|
||||
|
||||
use email_address_parser::EmailAddress;
|
||||
use lettre::{AsyncTransport, Message};
|
||||
|
||||
use std::process::{Command};
|
||||
use crate::helper::mail_queue::mailer::setup;
|
||||
use crate::Settings;
|
||||
|
||||
//TODO: replace this crap with lettre lib
|
||||
|
||||
pub fn render_string(str: String) -> String{
|
||||
str.replace("\"", "\\\"").replace("\'", "\\\'")
|
||||
}
|
||||
|
||||
pub fn send_mail(mail: Mail) -> Result<(), ()> {
|
||||
if (mail.to.is_empty() && mail.bcc.is_empty() && mail.cc.is_empty()) || mail.subject.is_empty() || mail.from.is_empty() || mail.body.is_empty() {
|
||||
error!("Couldn't deliver mail {} because to, subject, from or body missing! Deleting mail!", mail.uuid);
|
||||
return Err(())
|
||||
}
|
||||
|
||||
let body = render_string(mail.body);
|
||||
let subject = mail.subject.escape_default().to_string();
|
||||
|
||||
let mut arg = String::from("echo \"");
|
||||
arg.push_str(&body);
|
||||
arg.push_str("\" | mailx --append='FROM: ");
|
||||
arg.push_str(&mail.from);
|
||||
arg.push_str("' ");
|
||||
if !mail.cc.is_empty(){
|
||||
arg.push_str("--append='CC:");
|
||||
for mail in mail.cc{
|
||||
if EmailAddress::is_valid(&mail, None) {
|
||||
arg.push_str(&(mail + ","));
|
||||
}else{
|
||||
warn!("E-Mail worker - invalid E-Mail address: {}", mail);
|
||||
}
|
||||
}
|
||||
arg.push_str("' ")
|
||||
}
|
||||
if !mail.bcc.is_empty(){
|
||||
arg.push_str("--append='BCC:");
|
||||
for mail in mail.bcc{
|
||||
if EmailAddress::is_valid(&mail, None) {
|
||||
arg.push_str(&(mail + ","));
|
||||
}else{
|
||||
warn!("E-Mail worker - invalid E-Mail address: {}", mail);
|
||||
}
|
||||
}
|
||||
arg.push_str("' ")
|
||||
}
|
||||
match mail.reply_to{
|
||||
Some(reply_to) => {
|
||||
if EmailAddress::is_valid(&reply_to, None) {
|
||||
arg.push_str(&format!("--append='Reply-To: {} ' ", reply_to))
|
||||
}else{
|
||||
warn!("E-Mail worker - invalid E-Mail address: {}", reply_to);
|
||||
}
|
||||
},
|
||||
None => ()
|
||||
}
|
||||
arg.push_str("-s \"");
|
||||
arg.push_str(&subject);
|
||||
arg.push_str("\" ");
|
||||
for receiver in mail.to{
|
||||
if EmailAddress::is_valid(&receiver, None) {
|
||||
arg.push_str(&(receiver+" "));
|
||||
}else{
|
||||
warn!("E-Mail worker - invalid E-Mail address: {}", receiver);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("sending email with {}", arg);
|
||||
|
||||
match Command::new("sh").arg("-c").arg(arg).output(){
|
||||
Ok(output) => {
|
||||
if !output.status.success(){
|
||||
error!("Couldn't send mail: {} {} {}", output.status, String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout));
|
||||
Err(())
|
||||
}else {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
pub async fn send_mail(mail: Message, settings: &Settings) -> Result<(), ()> {
|
||||
let mailer = match setup(settings) {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
error!("Couldn't send mail: {}", e);
|
||||
error!("Error setting up AsyncSmtpTransportBuilder: {}", e);
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
match mailer.send(mail).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Error sending E-mail: {}", e);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,12 @@ use std::convert::TryInto;
|
|||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
/// Warning: You have to use #\[serde(default)] on each struct field! Otherwise every missing field will be interpreted as null
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Patch<T> {
|
||||
//No value for field transmitted, treat as keep old
|
||||
Missing,
|
||||
//Null for field transmitted, treat as set to NULL
|
||||
Null,
|
||||
Value(T),
|
||||
}
|
||||
|
@ -25,6 +28,11 @@ impl<T> From<Option<T>> for Patch<T> {
|
|||
}
|
||||
|
||||
impl<T> Into<Option<Option<T>>> for Patch<T> {
|
||||
/// Converts Patch into Option<Option<T>>, allowing null values
|
||||
///
|
||||
/// * Patch::Missing results in None -> do not update field
|
||||
/// * Patch::Value(T) results in Some(Some(T)) -> update field with T
|
||||
/// * Patch::Null results in Some(None) -> set field to null
|
||||
fn into(self) -> Option<Option<T>> {
|
||||
match self {
|
||||
Patch::Missing => None,
|
||||
|
@ -37,6 +45,11 @@ impl<T> Into<Option<Option<T>>> for Patch<T> {
|
|||
impl<T> TryInto<Option<T>> for Patch<T> {
|
||||
type Error = ();
|
||||
|
||||
/// Converts Patch into Option<T>, not allowing null values
|
||||
///
|
||||
/// * Patch::Missing results in None -> do not update field
|
||||
/// * Patch::Value(T) results in Some(T) -> update field with T
|
||||
/// * Patch::Null results in an error -> trying to set field to null which isn't allowed
|
||||
fn try_into(self) -> Result<Option<T>, Self::Error> {
|
||||
match self {
|
||||
Patch::Missing => Ok(None),
|
||||
|
|
|
@ -29,6 +29,9 @@ pub struct Mail{
|
|||
pub from: String,
|
||||
pub reply_to: String,
|
||||
pub communication_email_type_id: String,
|
||||
pub smtp_host: String,
|
||||
pub smtp_username: String,
|
||||
pub smtp_password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone)]
|
||||
|
@ -39,6 +42,8 @@ pub struct Api {
|
|||
#[derive(Debug, Deserialize, Default, Clone)]
|
||||
pub struct Billing {
|
||||
pub member_responsible_overwrites_permissions: bool,
|
||||
pub send_personnel_billing_to_email: bool,
|
||||
pub personnel_billing_email: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone)]
|
||||
|
@ -52,26 +57,13 @@ pub struct Settings {
|
|||
|
||||
impl Settings {
|
||||
pub fn new() -> Result<Self, ConfigError> {
|
||||
let mut s = Config::new();
|
||||
|
||||
// Start off by merging in the "default" configuration file
|
||||
s.merge(File::with_name("config/default"))?;
|
||||
|
||||
// Add in the current environment file
|
||||
// Default to 'development' env
|
||||
// Note that this file is _optional_
|
||||
let env = env::var("RUN_MODE").unwrap_or("development".into());
|
||||
s.merge(File::with_name(&format!("config/{}", env)).required(false))?;
|
||||
|
||||
// Add in a local configuration file
|
||||
// This file shouldn't be checked in to git
|
||||
s.merge(File::with_name("config/local").required(false))?;
|
||||
|
||||
// Add in settings from the environment (with a prefix of APP)
|
||||
// Eg.. `APP_DEBUG=1 ./target/app` would set the `debug` key
|
||||
s.merge(Environment::with_prefix("app"))?;
|
||||
|
||||
// You can deserialize (and thus freeze) the entire configuration as
|
||||
s.try_into()
|
||||
Config::builder()
|
||||
.add_source(config::File::with_name("config/default"))
|
||||
.add_source(config::File::with_name(&format!("config/{}", env)).required(false))
|
||||
// This file shouldn't be checked in to git:
|
||||
.add_source(config::File::with_name("config/local").required(false))
|
||||
.build()?.try_deserialize()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::helper::sitebuilder::model::alerts::{Alert, AlertClass};
|
||||
use crate::helper::sitebuilder::model::sidebar::{EventManagement, MemberManagement, ResourceManagement, Sidebar, Summary, Communicator, Settings};
|
||||
use crate::helper::sitebuilder::model::sidebar::{Communicator, EventBilling, EventManagement, MemberManagement, ResourceManagement, Settings, Sidebar, Summary};
|
||||
use crate::modules::member_management::model::member::Member;
|
||||
|
||||
impl Alert {
|
||||
|
@ -29,6 +29,10 @@ impl Sidebar {
|
|||
visible: member.has_permission(crate::permissions::modules::event_management::VIEW.to_string()),
|
||||
active: false,
|
||||
},
|
||||
event_billing: EventBilling {
|
||||
visible: member.has_permission(crate::permissions::modules::event_billing::VIEW.to_string()),
|
||||
active: false,
|
||||
},
|
||||
communicator: Communicator {
|
||||
visible: member.has_permission(crate::permissions::modules::communicator::VIEW.to_string()),
|
||||
active: false,
|
||||
|
|
|
@ -6,6 +6,7 @@ pub struct Sidebar {
|
|||
pub resource_management: ResourceManagement,
|
||||
pub member_management: MemberManagement,
|
||||
pub event_management: EventManagement,
|
||||
pub event_billing: EventBilling,
|
||||
pub communicator: Communicator,
|
||||
pub settings: Settings,
|
||||
}
|
||||
|
@ -34,6 +35,12 @@ pub struct EventManagement {
|
|||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EventBilling {
|
||||
pub visible: bool,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Communicator {
|
||||
pub visible: bool,
|
||||
|
|
|
@ -5,26 +5,30 @@ use rocket::State;
|
|||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::Settings;
|
||||
|
||||
pub fn get_timezone(settings: &State<Settings>, cookie: &SessionCookie) -> Tz {
|
||||
pub fn get_timezone(settings: &State<Settings>, cookie: Option<&SessionCookie>) -> Tz {
|
||||
let default_tz: Tz = match settings.application.default_timezone.parse() {
|
||||
Ok(tz) => tz,
|
||||
Err(e) => {
|
||||
panic!("Invalid configuration! Couldn't parse default_timezone: {}", e);
|
||||
}
|
||||
};
|
||||
match cookie.user.timezone.clone() {
|
||||
Some(tz) => match tz.parse() {
|
||||
Ok(tz) => tz,
|
||||
Err(e) => {
|
||||
warn!("Unparsable timezone for user: {}, falling back to default timezone. Error is {}", cookie.user.id, e);
|
||||
default_tz
|
||||
}
|
||||
},
|
||||
None => default_tz
|
||||
if let Some(cookie) = cookie {
|
||||
match cookie.user.timezone.clone() {
|
||||
Some(tz) => match tz.parse() {
|
||||
Ok(tz) => tz,
|
||||
Err(e) => {
|
||||
warn!("Unparsable timezone for user: {}, falling back to default timezone. Error is {}", cookie.user.id, e);
|
||||
default_tz
|
||||
}
|
||||
},
|
||||
None => default_tz
|
||||
}
|
||||
} else {
|
||||
default_tz
|
||||
}
|
||||
}
|
||||
|
||||
pub fn datetime_str_to_utc(settings: &State<Settings>, cookie: &SessionCookie, user_datetime: &str) -> Result<DateTime<Utc>, ParseError> {
|
||||
pub fn datetime_str_to_utc(settings: &State<Settings>, cookie: Option<&SessionCookie>, user_datetime: &str) -> Result<DateTime<Utc>, ParseError> {
|
||||
let tz = get_timezone(settings, cookie);
|
||||
|
||||
let naive = NaiveDateTime::parse_from_str(user_datetime, "%Y-%m-%dT%H:%M")?;
|
||||
|
@ -32,7 +36,7 @@ pub fn datetime_str_to_utc(settings: &State<Settings>, cookie: &SessionCookie, u
|
|||
Ok(local_datetime.with_timezone(&Utc))
|
||||
}
|
||||
|
||||
pub fn utc_to_local_user_time(settings: &State<Settings>, cookie: &SessionCookie, utc: DateTime<Utc>) -> String {
|
||||
pub fn utc_to_local_user_time(settings: &State<Settings>, cookie: Option<&SessionCookie>, utc: DateTime<Utc>) -> String {
|
||||
let tz = get_timezone(settings, cookie);
|
||||
let local_time = utc.with_timezone(&tz);
|
||||
local_time.format("%Y-%m-%dT%H:%M").to_string()
|
||||
|
@ -92,7 +96,7 @@ mod tests {
|
|||
let utcdate: DateTime<Utc> = Utc.ymd(2022, 1, 23).and_hms(12, 0, 0);
|
||||
|
||||
let rocket = rocket::build().manage(get_test_settings());
|
||||
let res = utc_to_local_user_time(State::get(&rocket).unwrap(), &get_test_cookies(), utcdate);
|
||||
let res = utc_to_local_user_time(State::get(&rocket).unwrap(), Some(&get_test_cookies()), utcdate);
|
||||
|
||||
assert_eq!(res, "2022-01-23T13:00");
|
||||
}
|
||||
|
@ -100,38 +104,38 @@ mod tests {
|
|||
#[test]
|
||||
pub fn test_datetime_str_to_utc_error() {
|
||||
let rocket = rocket::build().manage(get_test_settings());
|
||||
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23").is_err());
|
||||
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "2022-01-23").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_datetime_str_to_utc_error2() {
|
||||
let rocket = rocket::build().manage(get_test_settings());
|
||||
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T").is_err());
|
||||
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "2022-01-23T").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_datetime_str_to_utc_error3() {
|
||||
let rocket = rocket::build().manage(get_test_settings());
|
||||
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T15:00:00").is_err());
|
||||
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "2022-01-23T15:00:00").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_datetime_str_to_utc_error4() {
|
||||
let rocket = rocket::build().manage(get_test_settings());
|
||||
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T15").is_err());
|
||||
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "2022-01-23T15").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_datetime_str_to_utc_error5() {
|
||||
let rocket = rocket::build().manage(get_test_settings());
|
||||
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "Black Mamba").is_err());
|
||||
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "Black Mamba").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_datetime_str_to_utc_correct() {
|
||||
let rocket = rocket::build().manage(get_test_settings());
|
||||
let utcdate: DateTime<Utc> = Utc.ymd(2022, 1, 23).and_hms(12, 0, 0);
|
||||
assert_eq!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T13:00").unwrap(), utcdate);
|
||||
assert_eq!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "2022-01-23T13:00").unwrap(), utcdate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
47
src/main.rs
47
src/main.rs
|
@ -20,6 +20,7 @@ use std::process::Command;
|
|||
use std::sync::Arc;
|
||||
|
||||
use rocket::fs::FileServer;
|
||||
use rocket::tokio;
|
||||
use rocket_dyn_templates::handlebars::Handlebars;
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
|
@ -77,16 +78,24 @@ fn rocket() -> _ {
|
|||
// Initialize mail queue for second thread handling outgoing mails
|
||||
// We are using Arc to access mail queue in all threads
|
||||
let mail_queue = Arc::new(MailQueue::load_or_create_new());
|
||||
let c_lock = Arc::clone(&mail_queue);
|
||||
let c_lock = mail_queue.clone();
|
||||
let settings_clone = settings.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
match c_lock.process_next() {
|
||||
Ok(_) => {}
|
||||
Err(e) => error!("MailQueue poisoned: {}", e),
|
||||
let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
loop {
|
||||
let c_lock2 = c_lock.clone();
|
||||
let settings_clone2 = settings_clone.clone();
|
||||
|
||||
if c_lock2.mails.read().unwrap().len() != 0 {
|
||||
tokio::spawn(async move {
|
||||
c_lock2.process_next(&settings_clone2).await;
|
||||
});
|
||||
}
|
||||
thread::sleep(time::Duration::from_millis(500)) // Only check for new mails ever 500 ms
|
||||
}
|
||||
thread::sleep(time::Duration::from_millis(500)) // Only check for new mails ever 500 ms
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let mut mail_templates = MailTemplates {
|
||||
|
@ -114,9 +123,9 @@ fn rocket() -> _ {
|
|||
.manage(cookie_storage)
|
||||
.manage(mail_queue)
|
||||
.manage(mail_templates)
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
.mount( //TODO: find a way to get rip of this crap
|
||||
"/",
|
||||
routes![
|
||||
modules::dashboard::view::dashboard,
|
||||
modules::welcome::view::welcome_get::welcome_get,
|
||||
modules::welcome::view::welcome_post::welcome_post,
|
||||
|
@ -192,6 +201,9 @@ fn rocket() -> _ {
|
|||
modules::api::events::create::create_event,
|
||||
modules::api::events::update::update_event,
|
||||
modules::api::events::delete::delete_event,
|
||||
modules::api::events::update::close_event,
|
||||
modules::api::events::update::approve_times,
|
||||
modules::api::events::read::read_min_billing_states_for_event,
|
||||
modules::event_management::event_unit_positions::event_unit_positions,
|
||||
modules::event_management::event_unit_templates::event_unit_templates,
|
||||
modules::api::members::licenses::read::read_license_categories,
|
||||
|
@ -211,6 +223,9 @@ fn rocket() -> _ {
|
|||
modules::api::events::event_units::templates::update::update_position_for_template,
|
||||
modules::event_management::edit_event::edit_event,
|
||||
modules::api::events::instances::create::create_instance,
|
||||
modules::api::events::instances::create::add_position_instance,
|
||||
modules::api::events::instances::delete::delete_position_instance,
|
||||
modules::api::events::instances::update::patch_position_instance,
|
||||
modules::api::events::instances::read::read_instances,
|
||||
modules::api::events::instances::update::patch_instance,
|
||||
modules::api::events::event_units::position::create::create_event_unit_vehicle_position,
|
||||
|
@ -250,6 +265,18 @@ fn rocket() -> _ {
|
|||
modules::api::permissions::roles::update::api_remove_member_from_role,
|
||||
modules::api::permissions::roles::update::api_update_role,
|
||||
modules::print::event_note::print_event_note,
|
||||
modules::event_billing::event::event_view,
|
||||
modules::event_billing::close_event::close_event,
|
||||
modules::event_billing::edit_times::edit_times,
|
||||
modules::api::events::update::finish_personnel_billing,
|
||||
modules::event_billing::approve::approve,
|
||||
modules::event_billing::edit_personnel_billing::edit_personnel_billing,
|
||||
modules::api::billing_states::read::read_billing_states,
|
||||
modules::api::billing_rates::read::read_personnel_billing_rates,
|
||||
modules::api::personnel_billing::create::add_personnel_billing_entry,
|
||||
modules::api::personnel_billing::delete::delete_position_instance,
|
||||
modules::api::personnel_billing::read::get_personnel_billing,
|
||||
modules::api::events::update::approve,
|
||||
],
|
||||
)
|
||||
.mount("/css", FileServer::from("resources/css"))
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
pub mod read;
|
|
@ -0,0 +1,50 @@
|
|||
use std::convert::TryInto;
|
||||
use std::ops::Deref;
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::billing::personnel_billing_rates::{get_billing_rates, RawBillingRate};
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::modules::api::member_management::controller::parser::parse_member_cookie;
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
use crate::Settings;
|
||||
|
||||
#[derive(Queryable, Clone, Serialize)]
|
||||
pub struct BillingRate {
|
||||
billing_rate_id: uuid::Uuid,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
active: bool,
|
||||
payment_per_hour: String,
|
||||
lump_sum: String,
|
||||
}
|
||||
|
||||
impl From<RawBillingRate> for BillingRate {
|
||||
fn from(data: RawBillingRate) -> Self {
|
||||
BillingRate {
|
||||
billing_rate_id: data.billing_rate_id,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
active: data.active,
|
||||
payment_per_hour: data.payment_per_hour.to_string(),
|
||||
lump_sum: data.lump_sum.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/personnel_billing_rates", format = "json")]
|
||||
pub fn read_personnel_billing_rates(settings: &State<Settings>, cookie: SessionCookie) -> Result<Json<Vec<BillingRate>>, Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
|
||||
//TODO: add overwrite if member_responsible
|
||||
if !caller.has_permission(crate::permissions::modules::event_billing::VIEW.to_string()) {
|
||||
return Err(Json(ApiError::new(403, "Keine Berechtigung Abrechnungsstatusse abzurufen!".to_string()).to_wrapper()));
|
||||
}
|
||||
|
||||
match get_billing_rates(settings) {
|
||||
Ok(states) => Ok(Json(states.iter().map(|mut state| state.clone().into()).collect())),
|
||||
Err(e) => Err(translate_diesel(e)),
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
pub mod read;
|
|
@ -0,0 +1,24 @@
|
|||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::billing::states::{BillingState, get_billing_states};
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::modules::api::member_management::controller::parser::parse_member_cookie;
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
use crate::Settings;
|
||||
|
||||
#[get("/api/billing_states", format = "json")]
|
||||
pub fn read_billing_states(settings: &State<Settings>, cookie: SessionCookie) -> Result<Json<Vec<BillingState>>, Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
|
||||
//TODO: add overwrite if member_responsible
|
||||
if !caller.has_permission(crate::permissions::modules::event_billing::VIEW.to_string()) {
|
||||
return Err(Json(ApiError::new(403, "Keine Berechtigung Abrechnungsstatusse abzurufen!".to_string()).to_wrapper()));
|
||||
}
|
||||
|
||||
match get_billing_states(settings) {
|
||||
Ok(states) => Ok(Json(states)),
|
||||
Err(e) => Err(translate_diesel(e)),
|
||||
}
|
||||
}
|
|
@ -1,24 +1,22 @@
|
|||
use rocket::State;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
|
||||
use std::sync::Arc;
|
||||
|
||||
use lettre::Message;
|
||||
use rocket::serde::json::Json;
|
||||
use crate::modules::api::member_management::controller::parser::parse_member_cookie;
|
||||
use crate::helper::mail_queue::queue::{Mail, MailQueue};
|
||||
use rocket::State;
|
||||
|
||||
use std::sync::{Arc};
|
||||
|
||||
use crate::database::controller::api_communication_targets::{get_member_email_addresses, get_group_email_addresses, get_unit_email_addresses};
|
||||
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
|
||||
use crate::database::controller::members::check_access_to_resource;
|
||||
use crate::database::controller::api_communication_targets::{get_group_email_addresses, get_member_email_addresses, get_unit_email_addresses};
|
||||
use crate::database::controller::groups::get_group;
|
||||
|
||||
use crate::database::controller::members::check_access_to_resource;
|
||||
use crate::database::controller::units::get_unit;
|
||||
use crate::helper::mail_queue::queue::MailQueue;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::modules::api::member_management::controller::parser::parse_member_cookie;
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize)]
|
||||
pub struct ApiEmail{
|
||||
pub struct ApiEmail {
|
||||
pub(crate) to: Option<Vec<String>>,
|
||||
pub(crate) to_members: Option<Vec<uuid::Uuid>>,
|
||||
pub(crate) cc: Option<Vec<String>>,
|
||||
|
@ -33,117 +31,164 @@ pub struct ApiEmail{
|
|||
}
|
||||
|
||||
#[post("/api/communicator/email", format = "json", data = "<mail>")]
|
||||
pub fn create_email(mq: &State<Arc<MailQueue>>, settings: &State<Settings>, cookie: SessionCookie, mail: Json<ApiEmail>) -> Result<(), Json<ApiErrorWrapper>>{
|
||||
pub fn create_email(mq: &State<Arc<MailQueue>>, settings: &State<Settings>, cookie: SessionCookie, mail: Json<ApiEmail>) -> Result<(), Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
if !caller.has_permission(crate::permissions::modules::communicator::email::SEND.to_string()){
|
||||
if !caller.has_permission(crate::permissions::modules::communicator::email::SEND.to_string()) {
|
||||
return Err(Json(ApiError::new(403, "Keine Berechtigung Email zu versenden!".to_string()).to_wrapper()));
|
||||
}
|
||||
|
||||
let mail = mail.into_inner();
|
||||
let maildata = mail.into_inner();
|
||||
|
||||
let mut to: Vec<String> = vec![];
|
||||
let mut cc: Vec<String> = vec![];
|
||||
let mut bcc: Vec<String> = vec![];
|
||||
let mut mail = Message::builder()
|
||||
.from(settings.mail.from.clone().parse().unwrap())
|
||||
.reply_to(settings.mail.reply_to.clone().parse().unwrap());
|
||||
|
||||
match mail.to{
|
||||
Some(mut mail) => {
|
||||
to.append(mail.as_mut())
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match mail.to_members{
|
||||
Some(members) => {
|
||||
for member_id in members{
|
||||
match get_member_email_addresses(settings, member_id){
|
||||
Ok(mut addresses) => {
|
||||
to.append(addresses.as_mut());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
if let Some(receivers) = maildata.to {
|
||||
for receiver in receivers {
|
||||
match receiver.parse() {
|
||||
Ok(receiver) => {
|
||||
mail = mail.to(receiver);
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse to address: {}", e);
|
||||
return Err(Json(ApiError::new(400, "Das An Feld enthält ungültige E-Mail Adressen!".to_string()).to_wrapper()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match mail.cc{
|
||||
Some(mut mail) => {
|
||||
cc.append(mail.as_mut())
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match mail.cc_members{
|
||||
Some(members) => {
|
||||
for member_id in members{
|
||||
match get_member_email_addresses(settings, member_id){
|
||||
Ok(mut addresses) => {
|
||||
cc.append(addresses.as_mut());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match mail.bcc{
|
||||
Some(mut mail) => {
|
||||
bcc.append(mail.as_mut())
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match mail.bcc_members{
|
||||
Some(members) => {
|
||||
for member_id in members{
|
||||
match get_member_email_addresses(settings, member_id){
|
||||
Ok(mut addresses) => {
|
||||
bcc.append(addresses.as_mut());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
match mail.selected_groups{
|
||||
None => {}
|
||||
Some(groups) => {
|
||||
for group in groups{
|
||||
if !check_access_to_resource(settings, caller.entity_id, group, crate::permissions::modules::communicator::email::SEND){
|
||||
match get_group(settings, group){
|
||||
Ok(group) => {
|
||||
return Err(Json(ApiError::new(403, format!("Keine Berechtigung eine Email an die Gruppe {} zu schicken!", group.name)).to_wrapper()))
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
if let Some(to_members) = maildata.to_members {
|
||||
for member_id in to_members {
|
||||
match get_member_email_addresses(settings, member_id) {
|
||||
Ok(mut addresses) => {
|
||||
for address in addresses {
|
||||
match address.parse() {
|
||||
Ok(address) => {
|
||||
mail = mail.to(address);
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Couldn't parse members email address {}: {}", address, e);
|
||||
//Do not return error here, otherwise whole delivery would fail if one member has an falsy email address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match get_group_email_addresses(settings, group){
|
||||
Ok(mut emails) => {
|
||||
bcc.append(emails.as_mut());
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ccers) = maildata.cc {
|
||||
for cc in ccers {
|
||||
match cc.parse() {
|
||||
Ok(cc) => {
|
||||
mail = mail.cc(cc);
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse cc address: {}", e);
|
||||
return Err(Json(ApiError::new(400, "Das CC Feld enthält ungültige E-Mail Adressen!".to_string()).to_wrapper()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(cc_members) = maildata.cc_members {
|
||||
for member_id in cc_members {
|
||||
match get_member_email_addresses(settings, member_id) {
|
||||
Ok(mut addresses) => {
|
||||
for address in addresses {
|
||||
match address.parse() {
|
||||
Ok(address) => {
|
||||
mail = mail.cc(address);
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Couldn't parse members email address {}: {}", address, e);
|
||||
//Do not return error here, otherwise whole delivery would fail if one member has an falsy email address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(bccers) = maildata.bcc {
|
||||
for bcc in bccers {
|
||||
match bcc.parse() {
|
||||
Ok(bcc) => {
|
||||
mail = mail.bcc(bcc);
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse bcc address: {}", e);
|
||||
return Err(Json(ApiError::new(400, "Das BCC Feld enthält ungültige E-Mail Adressen!".to_string()).to_wrapper()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(bcc_members) = maildata.bcc_members {
|
||||
for member_id in bcc_members {
|
||||
match get_member_email_addresses(settings, member_id) {
|
||||
Ok(mut addresses) => {
|
||||
for address in addresses {
|
||||
match address.parse() {
|
||||
Ok(address) => {
|
||||
mail = mail.bcc(address);
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Couldn't parse members email address {}: {}", address, e);
|
||||
//Do not return error here, otherwise whole delivery would fail if one member has an falsy email address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(groups) = maildata.selected_groups {
|
||||
for group in groups {
|
||||
if !check_access_to_resource(settings, caller.entity_id, group, crate::permissions::modules::communicator::email::SEND) {
|
||||
match get_group(settings, group) {
|
||||
Ok(group) => {
|
||||
return Err(Json(ApiError::new(403, format!("Keine Berechtigung eine Email an die Gruppe {} zu schicken!", group.name)).to_wrapper()))
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
match get_group_email_addresses(settings, group) {
|
||||
Ok(mut addresses) => {
|
||||
for address in addresses {
|
||||
match address.parse() {
|
||||
Ok(address) => {
|
||||
mail = mail.bcc(address);
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Couldn't parse members email address {}: {}", address, e);
|
||||
//Do not return error here, otherwise whole delivery would fail if one member has an falsy email address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match mail.selected_units{
|
||||
match maildata.selected_units {
|
||||
None => {}
|
||||
Some(units) => {
|
||||
for unit in units{
|
||||
if !check_access_to_resource(settings, caller.entity_id, unit, crate::permissions::modules::communicator::email::SEND){
|
||||
match get_unit(settings, unit){
|
||||
for unit in units {
|
||||
if !check_access_to_resource(settings, caller.entity_id, unit, crate::permissions::modules::communicator::email::SEND) {
|
||||
match get_unit(settings, unit) {
|
||||
Ok(unit) => {
|
||||
return Err(Json(ApiError::new(403, format!("Keine Berechtigung eine Email an die Einheit {} zu schicken!", unit.name)).to_wrapper()))
|
||||
}
|
||||
|
@ -152,9 +197,19 @@ pub fn create_email(mq: &State<Arc<MailQueue>>, settings: &State<Settings>, cook
|
|||
}
|
||||
}
|
||||
}
|
||||
match get_unit_email_addresses(settings, unit){
|
||||
Ok(mut emails) => {
|
||||
bcc.append(emails.as_mut());
|
||||
match get_unit_email_addresses(settings, unit) {
|
||||
Ok(mut addresses) => {
|
||||
for address in addresses {
|
||||
match address.parse() {
|
||||
Ok(address) => {
|
||||
mail = mail.bcc(address);
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Couldn't parse members email address {}: {}", address, e);
|
||||
//Do not return error here, otherwise whole delivery would fail if one member has an falsy email address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
|
@ -164,15 +219,10 @@ pub fn create_email(mq: &State<Arc<MailQueue>>, settings: &State<Settings>, cook
|
|||
}
|
||||
}
|
||||
|
||||
if to.is_empty() && cc.is_empty() && bcc.is_empty(){
|
||||
return Err(Json(ApiError::new(422, "Es muss mindestens ein Empfänger angegeben werden!".to_string()).to_wrapper()))
|
||||
}
|
||||
|
||||
let composed_mail = Mail::new(settings.mail.from.clone(), to,mail.subject,cc,bcc,mail.reply_to,mail.body,None);
|
||||
let mail = mail.body(maildata.body).unwrap();
|
||||
|
||||
match mq.add_mail(composed_mail){
|
||||
match mq.add_mail(mail) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(Json(ApiError::new(500, "Couldn't add mail to mail queue!".to_string()).to_wrapper()))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use rocket::State;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use rocket::serde::json::Json;
|
||||
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_option_uuid};
|
||||
use crate::database::model::events::Event;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::entities::generate_entity;
|
||||
use crate::database::controller::events::add_event;
|
||||
use crate::database::model::events::Event;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_option_uuid};
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize)]
|
||||
pub struct CreateEventData{
|
||||
pub struct CreateEventData {
|
||||
pub(crate) name: String,
|
||||
pub(crate) start: String,
|
||||
pub(crate) end: String,
|
||||
|
@ -24,6 +25,7 @@ pub struct CreateEventData{
|
|||
pub(crate) related_group: Option<String>,
|
||||
pub(crate) other: Option<String>,
|
||||
pub(crate) other_intern: Option<String>,
|
||||
pub(crate) state: Option<i16>,
|
||||
}
|
||||
|
||||
#[post("/api/events", format = "json", data = "<create_event_data>")]
|
||||
|
@ -81,7 +83,7 @@ pub fn create_event(
|
|||
other: ecd.other,
|
||||
other_intern: ecd.other_intern,
|
||||
related_request: None,
|
||||
state: 0
|
||||
state: 2
|
||||
};
|
||||
|
||||
match add_event(settings, input){
|
||||
|
|
|
@ -3,13 +3,16 @@ use rocket::State;
|
|||
|
||||
use crate::database::controller::entities::generate_entity;
|
||||
use crate::database::controller::events::{add_instance, add_position_instances_for_instance};
|
||||
use crate::database::controller::events::instances::instance_positions::{create, RawPositionInstanceInsert};
|
||||
use crate::database::controller::events::instances::instances::RawEventUnitInstance;
|
||||
use crate::database::model::events::EventUnitInstanceDeprecated;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::time::datetime_str_to_utc;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
|
||||
use crate::modules::api::events::instances::read::PositionInstance;
|
||||
use crate::modules::api::member_management::controller::parser::{
|
||||
parse_member_cookie, parse_uuid_string,
|
||||
};
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize)]
|
||||
|
@ -21,7 +24,11 @@ pub struct CreateInstanceData {
|
|||
pub planned_end_time: Option<String>,
|
||||
}
|
||||
|
||||
#[post("/api/events/<event_id>/instances", format = "json", data = "<create_instance_data>")]
|
||||
#[post(
|
||||
"/api/events/<event_id>/instances",
|
||||
format = "json",
|
||||
data = "<create_instance_data>"
|
||||
)]
|
||||
pub fn create_instance(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
|
@ -29,51 +36,64 @@ pub fn create_instance(
|
|||
event_id: String,
|
||||
) -> Result<Json<RawEventUnitInstance>, Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member.clone())?;
|
||||
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
|
||||
if !caller
|
||||
.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string())
|
||||
{
|
||||
return Err(Json(
|
||||
ApiError::new(403, "Keine Berechtigung Einsätze zu bearbeiten!".to_string()).to_wrapper(),
|
||||
ApiError::new(
|
||||
403,
|
||||
"Keine Berechtigung Einsätze zu bearbeiten!".to_string(),
|
||||
)
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
let cid = create_instance_data.into_inner();
|
||||
|
||||
if parse_uuid_string(event_id.clone())? != cid.event_id {
|
||||
return Err(Json(
|
||||
ApiError::new(400, "Two different event_ids in body and parameter!".to_string()).to_wrapper(),
|
||||
))
|
||||
ApiError::new(
|
||||
400,
|
||||
"Two different event_ids in body and parameter!".to_string(),
|
||||
)
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
let entity_id = match generate_entity(settings) {
|
||||
Ok(entity) => entity,
|
||||
Err(_e) => return Err(Json(ApiError::new(500, "Konnte keine neue Entität anlegen.".to_string()).to_wrapper())),
|
||||
Err(_e) => {
|
||||
return Err(Json(
|
||||
ApiError::new(500, "Konnte keine neue Entität anlegen.".to_string()).to_wrapper(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let planned_start_time = match cid.planned_start_time {
|
||||
Some(dt) => match datetime_str_to_utc(settings, &cookie, &dt) {
|
||||
Some(dt) => match datetime_str_to_utc(settings, Some(&cookie), &dt) {
|
||||
Ok(dt) => Some(dt),
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse planned start time as DateTime: {}",e);
|
||||
warn!("Couldn't parse planned start time as DateTime: {}", e);
|
||||
return Err(Json(
|
||||
ApiError::new(400, "Couldn't parse planned start time!".to_string()).to_wrapper(),
|
||||
))
|
||||
ApiError::new(400, "Couldn't parse planned start time!".to_string())
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
},
|
||||
None => None
|
||||
None => None,
|
||||
};
|
||||
|
||||
let planned_end_time = match cid.planned_end_time {
|
||||
Some(dt) => match datetime_str_to_utc(settings, &cookie, &dt) {
|
||||
Some(dt) => match datetime_str_to_utc(settings, Some(&cookie), &dt) {
|
||||
Ok(dt) => Some(dt),
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse planned end time as DateTime: {}",e);
|
||||
warn!("Couldn't parse planned end time as DateTime: {}", e);
|
||||
return Err(Json(
|
||||
ApiError::new(400, "Couldn't parse planned end time!".to_string()).to_wrapper(),
|
||||
))
|
||||
));
|
||||
}
|
||||
},
|
||||
None => None
|
||||
None => None,
|
||||
};
|
||||
|
||||
|
||||
let instance = RawEventUnitInstance {
|
||||
instance_id: entity_id,
|
||||
template_id: parse_uuid_string(cid.template_id)?,
|
||||
|
@ -85,15 +105,127 @@ pub fn create_instance(
|
|||
real_end_time: None,
|
||||
billing_rate_id: None,
|
||||
billing_state_id: None,
|
||||
billing_state_author: None
|
||||
};
|
||||
|
||||
match add_position_instances_for_instance(settings, instance.instance_id, instance.template_id){
|
||||
match add_position_instances_for_instance(settings, instance.instance_id, instance.template_id)
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
Err(e) => return Err(translate_diesel(e)),
|
||||
}
|
||||
|
||||
match add_instance(settings, instance){
|
||||
Ok(instance) => return Ok(Json(instance)),
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
return match add_instance(settings, instance) {
|
||||
Ok(instance) => Ok(Json(instance)),
|
||||
Err(e) => Err(translate_diesel(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize)]
|
||||
pub struct CreatePositionInstanceData {
|
||||
pub(crate) instance_id: uuid::Uuid,
|
||||
pub(crate) position_id: uuid::Uuid,
|
||||
pub(crate) taken_by: Option<uuid::Uuid>,
|
||||
pub(crate) real_start_time: Option<String>,
|
||||
pub(crate) real_end_time: Option<String>,
|
||||
}
|
||||
|
||||
#[post(
|
||||
"/api/events/instances/<instance_id>/positions/<position_id>",
|
||||
format = "json", data = "<create_position_instance_data>"
|
||||
)]
|
||||
pub fn add_position_instance(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
instance_id: String,
|
||||
position_id: String,
|
||||
create_position_instance_data: Json<CreatePositionInstanceData>,
|
||||
) -> Result<Json<PositionInstance>, Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member.clone())?;
|
||||
//TODO: allow member_responsible for event to bypass this permission
|
||||
if !caller
|
||||
.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string())
|
||||
{
|
||||
return Err(Json(
|
||||
ApiError::new(
|
||||
403,
|
||||
"Keine Berechtigung Einsätze zu bearbeiten!".to_string(),
|
||||
)
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
|
||||
let create_position_instance_data = create_position_instance_data.into_inner();
|
||||
|
||||
if parse_uuid_string(instance_id)? != create_position_instance_data.instance_id {
|
||||
return Err(Json(
|
||||
ApiError::new(
|
||||
400,
|
||||
"Two different instance_ids in body and parameter!".to_string(),
|
||||
)
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
|
||||
if parse_uuid_string(position_id)? != create_position_instance_data.position_id {
|
||||
return Err(Json(
|
||||
ApiError::new(
|
||||
400,
|
||||
"Two different instance_ids in body and parameter!".to_string(),
|
||||
)
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
//TODO: require extra permissions to set real start time & real end time
|
||||
let real_start_time = match create_position_instance_data.real_start_time {
|
||||
Some(dt) => match datetime_str_to_utc(settings, Some(&cookie), &dt) {
|
||||
Ok(dt) => Some(dt),
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse real start time as DateTime: {}", e);
|
||||
return Err(Json(
|
||||
ApiError::new(400, "Couldn't parse real start time!".to_string())
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let real_end_time = match create_position_instance_data.real_end_time {
|
||||
Some(dt) => match datetime_str_to_utc(settings, Some(&cookie), &dt) {
|
||||
Ok(end) => {
|
||||
if let Some(start) = real_start_time {
|
||||
if start.ge(&end) {
|
||||
return Err(Json(ApiError::new(400, "Start time can't be later than the end time!".to_string()).to_wrapper()))
|
||||
}
|
||||
}
|
||||
Some(end)
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse real end time as DateTime: {}", e);
|
||||
return Err(Json(
|
||||
ApiError::new(400, "Couldn't parse real end time!".to_string())
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
let data = RawPositionInstanceInsert {
|
||||
position_instance_id: None,
|
||||
instance_id: create_position_instance_data.instance_id,
|
||||
position_id: create_position_instance_data.position_id,
|
||||
taken_by: create_position_instance_data.taken_by,
|
||||
real_start_time,
|
||||
real_end_time,
|
||||
};
|
||||
|
||||
match create(settings, data) {
|
||||
Ok(res) => {
|
||||
match PositionInstance::from_raw(&res, settings, &cookie) {
|
||||
Ok(pi) => Ok(Json(pi)),
|
||||
Err(_) => return Err(Json(ApiError::new(500, "Couldn't retrieve position instance!".to_string()).to_wrapper())),
|
||||
}
|
||||
},
|
||||
Err(e) => Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
use rocket::State;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use rocket::serde::json::Json;
|
||||
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::entities::remove_entity;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
|
||||
#[delete("/api/events/instances/<instance_id>", format = "json")]
|
||||
pub fn delete_instance(
|
||||
|
@ -20,7 +21,28 @@ pub fn delete_instance(
|
|||
));
|
||||
}
|
||||
|
||||
match remove_entity(settings, parse_uuid_string(instance_id)?){
|
||||
match remove_entity(settings, parse_uuid_string(instance_id)?) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete position_instance with specified position_instance_id
|
||||
/// Returns Ok() (200) if deleted successfully, otherwise ApiError
|
||||
#[delete("/api/events/position_instances/<position_instance_id>", format = "json")]
|
||||
pub fn delete_position_instance(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
position_instance_id: String,
|
||||
) -> Result<(), Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
|
||||
return Err(Json(
|
||||
ApiError::new(403, "Keine Berechtigung Einsätze zu bearbeiten!".to_string()).to_wrapper(),
|
||||
));
|
||||
}
|
||||
|
||||
match crate::database::controller::events::instances::instance_positions::delete(settings, parse_uuid_string(position_instance_id)?) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::events::{get_instance_positions, get_instance_vehicle_positions};
|
||||
use crate::database::controller::events::{get_eu_position, get_instance_vehicle_positions, get_position_instances};
|
||||
use crate::database::controller::events::instances::instance_positions::RawPositionInstance;
|
||||
use crate::database::controller::events::instances::instances::{get_instances, RawEventUnitInstance};
|
||||
use crate::database::model::events::{EventUnitInstanceDeprecated, EventUnitInstancePosition, EventUnitInstanceVehiclePosition};
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
|
@ -10,6 +11,8 @@ use crate::helper::time::utc_to_local_user_time;
|
|||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
use crate::modules::api::personnel_billing::calculation::calculate_hours;
|
||||
use crate::schema::personnel_billing::fulfilled_time;
|
||||
|
||||
/// This struct contains RawEventUnitInstance data but converts all UTC DateTimes to local time
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize, )]
|
||||
|
@ -23,26 +26,26 @@ pub struct EventUnitInstance {
|
|||
pub real_start_time: Option<String>,
|
||||
pub real_end_time: Option<String>,
|
||||
pub billing_rate_id: Option<uuid::Uuid>,
|
||||
pub billing_state_id: Option<String>,
|
||||
pub billing_state_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
impl EventUnitInstance {
|
||||
/// Convert RawEventUnitInstance to EventUnitInstance by converting Utc DateTimes to timestamps with user specified timezone
|
||||
pub fn from_raw(raw: RawEventUnitInstance, settings: &State<Settings>, cookie: &SessionCookie) -> EventUnitInstance {
|
||||
let planned_start_time = match raw.planned_start_time {
|
||||
Some(dt) => Some(utc_to_local_user_time(settings, cookie, dt)),
|
||||
Some(dt) => Some(utc_to_local_user_time(settings, Some(cookie), dt)),
|
||||
None => None
|
||||
};
|
||||
let planned_end_time = match raw.planned_end_time {
|
||||
Some(dt) => Some(utc_to_local_user_time(settings, cookie, dt)),
|
||||
Some(dt) => Some(utc_to_local_user_time(settings, Some(cookie), dt)),
|
||||
None => None
|
||||
};
|
||||
let real_start_time = match raw.real_start_time {
|
||||
Some(dt) => Some(utc_to_local_user_time(settings, cookie, dt)),
|
||||
Some(dt) => Some(utc_to_local_user_time(settings, Some(cookie), dt)),
|
||||
None => None
|
||||
};
|
||||
let real_end_time = match raw.real_end_time {
|
||||
Some(dt) => Some(utc_to_local_user_time(settings, cookie, dt)),
|
||||
Some(dt) => Some(utc_to_local_user_time(settings, Some(cookie), dt)),
|
||||
None => None
|
||||
};
|
||||
|
||||
|
@ -61,6 +64,66 @@ impl EventUnitInstance {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize)]
|
||||
pub struct PositionInstance {
|
||||
pub(crate) position_instance_id: uuid::Uuid,
|
||||
pub(crate) instance_id: uuid::Uuid,
|
||||
pub(crate) position_id: uuid::Uuid,
|
||||
pub(crate) position_name: String,
|
||||
pub(crate) position_description: Option<String>,
|
||||
pub(crate) position_requirements: Option<serde_json::Value>,
|
||||
pub(crate) taken_by: Option<uuid::Uuid>,
|
||||
pub(crate) real_start_time: Option<String>,
|
||||
pub(crate) real_end_time: Option<String>,
|
||||
pub(crate) billable: Option<bool>,
|
||||
pub(crate) fulfilled_time: Option<i32>,
|
||||
}
|
||||
|
||||
impl PositionInstance {
|
||||
/// Convert RawPositionInstance to PositionInstance by converting Utc DateTimes to timestamps with user specified timezone
|
||||
pub fn from_raw(raw: &RawPositionInstance, settings: &State<Settings>, cookie: &SessionCookie) -> Result<PositionInstance, ()> {
|
||||
let real_start_time = match raw.real_start_time {
|
||||
Some(dt) => Some(utc_to_local_user_time(settings, Some(cookie), dt)),
|
||||
None => None
|
||||
};
|
||||
let real_end_time = match raw.real_end_time {
|
||||
Some(dt) => Some(utc_to_local_user_time(settings, Some(cookie), dt)),
|
||||
None => None
|
||||
};
|
||||
let position_data = match get_eu_position(settings, raw.position_id) {
|
||||
Ok(position) => position,
|
||||
Err(e) => {
|
||||
error!("Couldn't retrieve position data for RawPositionInstance -> PositionInstance conversion: {}. Maybe there are position instances with invalid position_ids?", e);
|
||||
return Err(())
|
||||
}
|
||||
};
|
||||
//Calculate hours
|
||||
let other_fulfilled_time = if let Some(start) = raw.real_start_time {
|
||||
if let Some(end) = raw.real_end_time {
|
||||
calculate_hours(start, end)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(PositionInstance {
|
||||
position_instance_id: raw.position_instance_id,
|
||||
instance_id: raw.instance_id,
|
||||
position_id: raw.position_id,
|
||||
position_name: position_data.name,
|
||||
position_description: position_data.description,
|
||||
position_requirements: position_data.requirements,
|
||||
taken_by: raw.taken_by,
|
||||
real_start_time,
|
||||
real_end_time,
|
||||
billable: raw.billable,
|
||||
fulfilled_time: other_fulfilled_time,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/events/<event_id>/instances", format = "json", rank = 1)]
|
||||
pub fn read_instances(
|
||||
settings: &State<Settings>,
|
||||
|
@ -87,21 +150,29 @@ pub fn read_instances(
|
|||
Ok(Json(res))
|
||||
}
|
||||
|
||||
/// Get a list of all position instances for specific instance_id
|
||||
#[get("/api/events/instances/<instance_id>/positions", format = "json", rank = 1)]
|
||||
pub fn read_positions_for_instance(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
instance_id: String,
|
||||
) -> Result<Json<Vec<EventUnitInstancePosition>>, Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
pub fn read_positions_for_instance(settings: &State<Settings>, cookie: SessionCookie, instance_id: String) -> Result<Json<Vec<PositionInstance>>, Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member.clone())?;
|
||||
//TODO: allow member_responsible for event to bypass this permission
|
||||
if !caller.has_permission(crate::permissions::modules::event_management::events::VIEW.to_string()) {
|
||||
return Err(Json(
|
||||
ApiError::new(403, "Keine Berechtigung Einsätze abzurufen!".to_string()).to_wrapper(),
|
||||
));
|
||||
return Err(Json(ApiError::new(403, "Keine Berechtigung Einsätze abzurufen!".to_string()).to_wrapper()));
|
||||
}
|
||||
|
||||
match get_instance_positions(settings, parse_uuid_string(instance_id)?){
|
||||
Ok(pos) => Ok(Json(pos)),
|
||||
match get_position_instances(settings, parse_uuid_string(instance_id)?) {
|
||||
Ok(pos) => {
|
||||
let mut res = vec![];
|
||||
for position in pos {
|
||||
//try to convert RawPositionInstance to PositionInstance.
|
||||
match PositionInstance::from_raw(&position, settings, &cookie) {
|
||||
Ok(pi) => res.push(pi),
|
||||
Err(_) => {
|
||||
return Err(Json(ApiError::new(500, "Couldn't retrieve position instance".to_string()).to_wrapper()))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Json(res))
|
||||
},
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,13 +4,14 @@ use rocket::serde::json::Json;
|
|||
use rocket::State;
|
||||
|
||||
use crate::database::controller::events::{change_position_instances, get_event};
|
||||
use crate::database::controller::events::instances::instances::{RawEventUnitInstanceChangeset, update_instance};
|
||||
use crate::database::controller::events::instances::instance_positions::RawPositionInstanceChangeset;
|
||||
use crate::database::controller::events::instances::instances::{get_instance, get_instances, RawEventUnitInstanceChangeset, update_instance};
|
||||
use crate::helper::serde_patch::Patch;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::time::datetime_str_to_utc;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::modules::api::events::instances::read::EventUnitInstance;
|
||||
use crate::modules::api::events::instances::read::{EventUnitInstance, PositionInstance};
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
use crate::modules::event_management::check_position_requirements::{check_position_requirements, RequirementParserError};
|
||||
|
@ -27,11 +28,33 @@ pub fn put_entity_in_position(
|
|||
|
||||
let position_id = parse_uuid_string(position_id)?;
|
||||
let entity_id = parse_uuid_string(entity_id)?;
|
||||
let instance_id = parse_uuid_string(instance_id)?;
|
||||
|
||||
let instance = match get_instance(settings, instance_id) {
|
||||
Ok(instance) => instance,
|
||||
Err(e) =>
|
||||
{
|
||||
warn!("Instance not found: {}", e);
|
||||
return Err(Json(ApiError::new(404, "instance not found!".to_string()).to_wrapper()));
|
||||
}
|
||||
};
|
||||
let event = match get_event(settings, instance.event_id) {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
warn!("Event not found: {}", e);
|
||||
return Err(Json(
|
||||
ApiError::new(404, "event for instance not found!".to_string()).to_wrapper()))
|
||||
}
|
||||
};
|
||||
if event.state >= 4 {
|
||||
return Err(Json(
|
||||
ApiError::new(400, "Dieser Einsatz wurde bereits geschlossen!".to_string()).to_wrapper()))
|
||||
}
|
||||
|
||||
if caller.entity_id == entity_id {
|
||||
match check_position_requirements(settings, position_id, entity_id){
|
||||
match check_position_requirements(settings, position_id, entity_id) {
|
||||
Ok(res) => {
|
||||
if !res{ //if member tries to add himself to a position, but don't fulfill the position requirements AND don't have the event edit permission (overwrite), abort
|
||||
if !res { //if member tries to add himself to a position, but don't fulfill the position requirements AND don't have the event edit permission (overwrite), abort
|
||||
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
|
||||
return Err(Json(
|
||||
ApiError::new(403, "Keine Berechtigung Einsätze zu bearbeiten!".to_string()).to_wrapper(),
|
||||
|
@ -51,15 +74,13 @@ pub fn put_entity_in_position(
|
|||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
|
||||
return Err(Json(
|
||||
ApiError::new(403, "Keine Berechtigung Einsätze zu bearbeiten!".to_string()).to_wrapper(),
|
||||
));
|
||||
}
|
||||
} else if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
|
||||
return Err(Json(
|
||||
ApiError::new(403, "Keine Berechtigung Einsätze zu bearbeiten!".to_string()).to_wrapper(),
|
||||
));
|
||||
}
|
||||
|
||||
match change_position_instances(settings, parse_uuid_string(instance_id)?, position_id, Some(entity_id)){
|
||||
match change_position_instances(settings, instance_id, position_id, Some(entity_id)) {
|
||||
Ok(pos) => Ok(Json(pos)),
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
}
|
||||
|
@ -79,7 +100,30 @@ pub fn remove_entity_from_position(
|
|||
));
|
||||
}
|
||||
|
||||
match change_position_instances(settings, parse_uuid_string(instance_id)?, parse_uuid_string(position_id)?, None) {
|
||||
let instance_id = parse_uuid_string(instance_id)?;
|
||||
|
||||
let instance = match get_instance(settings, instance_id) {
|
||||
Ok(instance) => instance,
|
||||
Err(e) =>
|
||||
{
|
||||
warn!("Instance not found: {}", e);
|
||||
return Err(Json(ApiError::new(404, "instance not found!".to_string()).to_wrapper()));
|
||||
}
|
||||
};
|
||||
let event = match get_event(settings, instance.event_id) {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
warn!("Event not found: {}", e);
|
||||
return Err(Json(
|
||||
ApiError::new(404, "event for instance not found!".to_string()).to_wrapper()))
|
||||
}
|
||||
};
|
||||
if event.state >= 4 {
|
||||
return Err(Json(
|
||||
ApiError::new(400, "Dieser Einsatz wurde bereits geschlossen!".to_string()).to_wrapper()))
|
||||
}
|
||||
|
||||
match change_position_instances(settings, instance_id, parse_uuid_string(position_id)?, None) {
|
||||
Ok(pos) => Ok(Json(pos)),
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
}
|
||||
|
@ -101,7 +145,7 @@ pub struct PatchInstanceData {
|
|||
#[serde(default)]
|
||||
pub billing_rate_id: Patch<uuid::Uuid>,
|
||||
#[serde(default)]
|
||||
pub billing_state_id: Patch<String>,
|
||||
pub billing_state_id: Patch<uuid::Uuid>,
|
||||
}
|
||||
|
||||
#[patch("/api/events/<event_id>/instances/<instance_id>", format = "json", data = "<patch_instance_data>")]
|
||||
|
@ -181,7 +225,7 @@ pub fn patch_instance(
|
|||
Patch::Missing => None,
|
||||
Patch::Null => Some(None),
|
||||
Patch::Value(v) => {
|
||||
match datetime_str_to_utc(settings, &cookie, &v) {
|
||||
match datetime_str_to_utc(settings, Some(&cookie), &v) {
|
||||
Ok(v) => Some(Some(v)),
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse planned start time as DateTime: {}",e);
|
||||
|
@ -196,7 +240,7 @@ pub fn patch_instance(
|
|||
Patch::Missing => None,
|
||||
Patch::Null => Some(None),
|
||||
Patch::Value(v) => {
|
||||
match datetime_str_to_utc(settings, &cookie, &v) {
|
||||
match datetime_str_to_utc(settings, Some(&cookie), &v) {
|
||||
Ok(v) => Some(Some(v)),
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse planned end time as DateTime: {}",e);
|
||||
|
@ -211,7 +255,7 @@ pub fn patch_instance(
|
|||
Patch::Missing => None,
|
||||
Patch::Null => Some(None),
|
||||
Patch::Value(v) => {
|
||||
match datetime_str_to_utc(settings, &cookie, &v) {
|
||||
match datetime_str_to_utc(settings, Some(&cookie), &v) {
|
||||
Ok(v) => Some(Some(v)),
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse real start time as DateTime: {}",e);
|
||||
|
@ -226,7 +270,7 @@ pub fn patch_instance(
|
|||
Patch::Missing => None,
|
||||
Patch::Null => Some(None),
|
||||
Patch::Value(v) => {
|
||||
match datetime_str_to_utc(settings, &cookie, &v) {
|
||||
match datetime_str_to_utc(settings, Some(&cookie), &v) {
|
||||
Ok(v) => Some(Some(v)),
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse real end time as DateTime: {}",e);
|
||||
|
@ -254,4 +298,138 @@ pub fn patch_instance(
|
|||
Ok(res) => Ok(Json(EventUnitInstance::from_raw(res, settings, &cookie))),
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Queryable, Deserialize)]
|
||||
pub struct PatchPositionInstanceData {
|
||||
#[serde(default)]
|
||||
pub(crate) instance_id: Patch<uuid::Uuid>,
|
||||
#[serde(default)]
|
||||
pub(crate) position_id: Patch<uuid::Uuid>,
|
||||
#[serde(default)]
|
||||
pub(crate) taken_by: Patch<uuid::Uuid>,
|
||||
#[serde(default)]
|
||||
pub(crate) position_instance_id: uuid::Uuid,
|
||||
#[serde(default)]
|
||||
pub(crate) real_start_time: Patch<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) real_end_time: Patch<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) billable: Patch<bool>,
|
||||
}
|
||||
|
||||
#[patch("/api/events/position_instances/<position_instance_id>", format = "json", data = "<patch_position_instance_data>")]
|
||||
pub fn patch_position_instance(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
patch_position_instance_data: Json<PatchPositionInstanceData>,
|
||||
position_instance_id: String,
|
||||
) -> Result<Json<PositionInstance>, Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member.clone())?;
|
||||
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
|
||||
return Err(Json(
|
||||
ApiError::new(403, "Keine Berechtigung Einsätze zu bearbeiten!".to_string()).to_wrapper(),
|
||||
));
|
||||
}
|
||||
|
||||
let pid = patch_position_instance_data.into_inner();
|
||||
let position_instance_id = parse_uuid_string(position_instance_id)?;
|
||||
|
||||
if position_instance_id != pid.position_instance_id {
|
||||
return Err(Json(ApiError::new(400, "position_instance_id in URI doesn't match position_instance_id in body data.".to_string()).to_wrapper()))
|
||||
}
|
||||
|
||||
let instance_id = match pid.instance_id.try_into() {
|
||||
Ok(res) => res,
|
||||
Err(_) => {
|
||||
return Err(Json(ApiError::new(400, "Cannot set instance_id to null!".to_string()).to_wrapper()))
|
||||
}
|
||||
};
|
||||
|
||||
let position_id = match pid.position_id.try_into() {
|
||||
Ok(res) => res,
|
||||
Err(_) => return Err(Json(ApiError::new(400, "Cannot set position_id to null!".to_string()).to_wrapper()))
|
||||
};
|
||||
|
||||
let real_start_time = match pid.real_start_time {
|
||||
Patch::Missing => None,
|
||||
Patch::Null => Some(None),
|
||||
Patch::Value(v) => {
|
||||
match datetime_str_to_utc(settings, Some(&cookie), &v) {
|
||||
Ok(v) => Some(Some(v)),
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse real start time as DateTime: {}",e);
|
||||
return Err(Json(
|
||||
ApiError::new(400, "Couldn't parse real start time!".to_string()).to_wrapper(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let real_end_time = match pid.real_end_time {
|
||||
Patch::Missing => None,
|
||||
Patch::Null => Some(None),
|
||||
Patch::Value(v) => {
|
||||
match datetime_str_to_utc(settings, Some(&cookie), &v) {
|
||||
Ok(end) => {
|
||||
match real_start_time {
|
||||
Some(start) => match start {
|
||||
Some(start) => {
|
||||
//start and end time are set, check if start is after end
|
||||
if start.ge(&end) {
|
||||
return Err(Json(ApiError::new(400, "Start time can't be later than the end time!".to_string()).to_wrapper()))
|
||||
}
|
||||
},
|
||||
None => {}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
Some(Some(end))
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse real end time as DateTime: {}",e);
|
||||
return Err(Json(
|
||||
ApiError::new(400, "Couldn't parse real end time!".to_string()).to_wrapper(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let billable = pid.billable.into();
|
||||
|
||||
if let Some(is_bill) = billable {
|
||||
if let Some(is_bill2) = is_bill {
|
||||
if !is_bill2 {
|
||||
match crate::database::controller::billing::personnel_billing::remove(settings, position_instance_id) {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
error!("Couldn't delete personnel_billing entries left after billable was set to false: {}", e);
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let data = RawPositionInstanceChangeset {
|
||||
instance_id,
|
||||
position_id,
|
||||
taken_by: pid.taken_by.into(),
|
||||
position_instance_id,
|
||||
real_start_time,
|
||||
real_end_time,
|
||||
billable,
|
||||
};
|
||||
|
||||
match crate::database::controller::events::instances::instance_positions::update(settings, data) {
|
||||
Ok(res) => {
|
||||
match PositionInstance::from_raw(&res, settings, &cookie) {
|
||||
Ok(res) => Ok(Json(res)),
|
||||
Err(_) => return Err(Json(ApiError::new(500, "Couldn't retrieve changed PositionInstance!".to_string()).to_wrapper())),
|
||||
}
|
||||
},
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
|
@ -2,7 +2,8 @@ use chrono::{Duration, Local, NaiveDateTime};
|
|||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::events::{get_event, get_event_count, get_events, get_events_for_member_in_future, get_instance_positions};
|
||||
use crate::database::controller::billing::states::{get_billing_states_for_event, get_min_billing_states_for_event};
|
||||
use crate::database::controller::events::{get_event, get_event_count, get_events, get_events_for_member_in_future, get_instance_positions_name_description, get_position_instances};
|
||||
use crate::database::controller::events::instances::instances::get_instances;
|
||||
use crate::database::model::events::Event;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
|
@ -158,7 +159,7 @@ pub fn check_event_cast_status(settings: &State<Settings>, cookie: SessionCookie
|
|||
let mut open_positions : Vec<OpenPosition> = vec![];
|
||||
|
||||
for instance in instances{
|
||||
let positions = match get_instance_positions(settings, instance.instance_id){
|
||||
let positions = match get_instance_positions_name_description(settings, instance.instance_id) {
|
||||
Ok(positions) => positions,
|
||||
Err(e) => {
|
||||
error!("Couldn't get instance positions for event cast status check.");
|
||||
|
@ -176,11 +177,45 @@ pub fn check_event_cast_status(settings: &State<Settings>, cookie: SessionCookie
|
|||
}
|
||||
|
||||
let full = open_positions.len() == 0;
|
||||
let open_positions = if full{None}else{Some(open_positions)};
|
||||
let open_positions = if full { None } else { Some(open_positions) };
|
||||
|
||||
Ok(Json(EventCastStatus{
|
||||
Ok(Json(EventCastStatus {
|
||||
event_id: event_id,
|
||||
full,
|
||||
open_positions
|
||||
open_positions,
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
/// Returns list of all non null billing_states for specific event
|
||||
#[get("/api/events/<event_id>/billing_states", format = "json", rank = 3)]
|
||||
pub fn read_billing_states_for_event(settings: &State<Settings>, cookie: SessionCookie, event_id: String) -> Result<Json<Vec<String>>, Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
|
||||
if !caller.has_permission(crate::permissions::modules::event_management::VIEW.to_string()) {
|
||||
return Err(Json(ApiError::new(403, "Keine Berechtigung Einsätze abzurufen!".to_string()).to_wrapper()))
|
||||
}
|
||||
|
||||
match get_billing_states_for_event(settings, parse_uuid_string(event_id)?) {
|
||||
Ok(states) => Ok(Json(states)),
|
||||
Err(e) => Err(translate_diesel(e)),
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// Returns minimal billing_state for specific event
|
||||
/// Ignores event units with null as billing_state_id
|
||||
/// Used to determine if a specific billing state is fulfilled for all instances on one event
|
||||
#[get("/api/events/<event_id>/billing_states/min", format = "json", rank = 3)]
|
||||
pub fn read_min_billing_states_for_event(settings: &State<Settings>, cookie: SessionCookie, event_id: String) -> Result<Json<Option<uuid::Uuid>>, Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
|
||||
if !caller.has_permission(crate::permissions::modules::event_management::VIEW.to_string()) {
|
||||
return Err(Json(ApiError::new(403, "Keine Berechtigung Einsätze abzurufen!".to_string()).to_wrapper()))
|
||||
}
|
||||
|
||||
match get_min_billing_states_for_event(settings, parse_uuid_string(event_id)?) {
|
||||
Ok(state) => Ok(Json(state)),
|
||||
Err(e) => Err(translate_diesel(e)),
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use lettre::Message;
|
||||
use lettre::message::Mailbox;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
|
@ -11,7 +13,7 @@ use crate::database::controller::organisers::get_organiser;
|
|||
use crate::database::controller::permissions::get_members_with_permission;
|
||||
use crate::database::model::event_requests::EventRequest;
|
||||
use crate::database::model::organisers::Organiser;
|
||||
use crate::helper::mail_queue::queue::{Mail, MailQueue};
|
||||
use crate::helper::mail_queue::queue::MailQueue;
|
||||
use crate::helper::mail_templates::MailTemplates;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::settings::Settings;
|
||||
|
@ -166,15 +168,31 @@ fn send_event_request_published_emails(mt: &State<MailTemplates>, mq: &State<Arc
|
|||
None => None
|
||||
};
|
||||
|
||||
for receiver in receivers{
|
||||
let emails = match get_member_email_addresses(settings, receiver){
|
||||
Ok(emails) => emails,
|
||||
for receiver in receivers {
|
||||
let mut mail = Message::builder()
|
||||
.from(settings.mail.from.clone().parse().unwrap())
|
||||
.reply_to(settings.mail.reply_to.clone().parse().unwrap())
|
||||
.subject("Einsatz Online - Passwort Zurücksetzen");
|
||||
|
||||
match get_member_email_addresses(settings, receiver) {
|
||||
Ok(emails) => {
|
||||
for email in emails {
|
||||
match email.parse() {
|
||||
Ok(email) => {
|
||||
mail = mail.to(email);
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Couldn't parse email address {}: {}", email, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Couldn't get email addresses for member: {}", e);
|
||||
continue
|
||||
}
|
||||
};
|
||||
let member = match get_member_by_uuid(receiver, settings){
|
||||
let member = match get_member_by_uuid(receiver, settings) {
|
||||
Some(member) => member,
|
||||
None => {
|
||||
error!("No member found for id {}", receiver);
|
||||
|
@ -182,21 +200,21 @@ fn send_event_request_published_emails(mt: &State<MailTemplates>, mq: &State<Arc
|
|||
}
|
||||
};
|
||||
|
||||
let nerpe = NewEventRequestPublishedEmail{
|
||||
let nerpe = NewEventRequestPublishedEmail {
|
||||
firstname: member.firstname,
|
||||
request: request.clone(),
|
||||
frontpage: settings.application.url.clone(),
|
||||
support_email: settings.application.user_support_email.clone(),
|
||||
organiser: organiser.clone()
|
||||
organiser: organiser.clone(),
|
||||
};
|
||||
let body = match mt.registry.render("new_event_request_published-de", &nerpe){
|
||||
let mail = mail.body(match mt.registry.render("new_event_request_published-de", &nerpe) {
|
||||
Ok(body) => body,
|
||||
Err(e) => {
|
||||
error!("Couldn't render email template: {}", e);
|
||||
return},
|
||||
};
|
||||
return
|
||||
},
|
||||
}).unwrap();
|
||||
|
||||
let mail = Mail::new(settings.mail.from.clone(), emails, format!("[{}] - Neue Einsatzanfrage", settings.application.name.clone()), vec![], vec![], Some(settings.mail.reply_to.clone()), body, None); //TODO: Add deliver_until
|
||||
match mq.add_mail(mail){
|
||||
Ok(_) => {},
|
||||
Err(_) => {}
|
||||
|
|
|
@ -1,14 +1,27 @@
|
|||
use rocket::State;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use rocket::serde::json::Json;
|
||||
use crate::modules::api::events::create::CreateEventData;
|
||||
use crate::database::model::events::Event;
|
||||
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_option_uuid, parse_uuid_string};
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::expression::ops::Mul;
|
||||
use lettre::{AsyncTransport, Message};
|
||||
use lettre::message::{Attachment, MultiPart, SinglePart};
|
||||
use lettre::message::header::ContentType;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::billing::states::{get_billing_state, get_billing_states};
|
||||
use crate::database::controller::events::{change_event, finish_billing, get_event};
|
||||
use crate::database::controller::events::instances::instance_positions::RawPositionInstanceChangeset;
|
||||
use crate::database::controller::events::instances::instances::get_instances;
|
||||
use crate::database::controller::members::check_access_to_resource;
|
||||
use crate::database::model::events::Event;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::database::controller::events::change_event;
|
||||
use crate::MailQueue;
|
||||
use crate::modules::api::events::create::CreateEventData;
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_option_uuid, parse_uuid_string};
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
use crate::modules::event_billing::generate_billing_csv::{generate_billing_csv, save_billing_csv};
|
||||
|
||||
#[put("/api/events/<entity_id>", format = "json", data = "<update_event_data>")]
|
||||
pub fn update_event(
|
||||
|
@ -18,6 +31,7 @@ pub fn update_event(
|
|||
update_event_data: Json<CreateEventData>,
|
||||
) -> Result<Json<Event>, Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
//TODO: Check if caller is member_responsible -> if so skip this check
|
||||
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
|
||||
return Err(Json(
|
||||
ApiError::new(403, "Keine Berechtigung Einsatz zu bearbeiten!".to_string()).to_wrapper(),
|
||||
|
@ -42,7 +56,14 @@ pub fn update_event(
|
|||
}
|
||||
};
|
||||
|
||||
let input: Event = Event{
|
||||
let state = match ecd.state {
|
||||
Some(state) => state,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
//TODO: Remove this parse_option_uuid crap. Just use rocket + serde deserialisation by replacing string to proper type in CreateEventData
|
||||
//TODO: related_request
|
||||
let input: Event = Event {
|
||||
entity_id: parse_uuid_string(entity_id)?,
|
||||
name: ecd.name,
|
||||
start,
|
||||
|
@ -57,11 +78,202 @@ pub fn update_event(
|
|||
other: ecd.other,
|
||||
other_intern: ecd.other_intern,
|
||||
related_request: None,
|
||||
state: 0
|
||||
state,
|
||||
};
|
||||
|
||||
match change_event(settings, input){
|
||||
match change_event(settings, input) {
|
||||
Ok(event) => Ok(Json(event)),
|
||||
Err(e) => Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/api/events/<entity_id>/close", format = "json")]
|
||||
pub fn close_event(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
entity_id: String,
|
||||
) -> Result<(), Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
|
||||
let event_id = parse_uuid_string(entity_id)?;
|
||||
|
||||
let event = match get_event(settings, event_id) {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
warn!("{} tried to close event with invalid event_id {}: {}", caller.entity_id, event_id, e);
|
||||
return Err(Json(ApiError::new(404, "No event with this entity_id found.".to_string()).to_wrapper()));
|
||||
}
|
||||
};
|
||||
|
||||
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
|
||||
match event.member_responsible {
|
||||
Some(resp) => {
|
||||
if caller.entity_id != resp {
|
||||
warn!("No permissions to close event for member {}", caller.entity_id);
|
||||
return Err(Json(ApiError::new(403, "No permissions to edit event.".to_string()).to_wrapper()));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("No permissions to close event for member {}", caller.entity_id);
|
||||
return Err(Json(ApiError::new(403, "No permissions to edit event.".to_string()).to_wrapper()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match crate::database::controller::events::close_event(settings, event_id) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/api/events/<entity_id>/approve_times", format = "json")]
|
||||
pub fn approve_times(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
entity_id: String,
|
||||
) -> Result<(), Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
|
||||
let event_id = parse_uuid_string(entity_id)?;
|
||||
|
||||
let event = match get_event(settings, event_id) {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
warn!("{} tried to close event with invalid event_id {}: {}", caller.entity_id, event_id, e);
|
||||
return Err(Json(ApiError::new(404, "No event with this entity_id found.".to_string()).to_wrapper()));
|
||||
}
|
||||
};
|
||||
|
||||
if !caller.has_permission(crate::permissions::modules::event_billing::start_end_times::EDIT.to_string()) {
|
||||
match event.member_responsible {
|
||||
Some(resp) => {
|
||||
if caller.entity_id != resp {
|
||||
warn!("No permissions to approve times for member {}", caller.entity_id);
|
||||
return Err(Json(ApiError::new(403, "No permissions to edit event.".to_string()).to_wrapper()));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("No permissions to approve times for member {}", caller.entity_id);
|
||||
return Err(Json(ApiError::new(403, "No permissions to edit event.".to_string()).to_wrapper()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Set missing real_start_time and/or real_end_time to planned_start_time/planned_end_time for all instances with event_id
|
||||
match crate::database::controller::events::instances::instances::set_missing_real_times_to_planned_times(settings, event_id) {
|
||||
Ok(_) => (),
|
||||
Err(_) => {
|
||||
return Err(Json(ApiError::new(500, "Couldn't set missing real times to planned times for instances.".to_string()).to_wrapper()));
|
||||
}
|
||||
}
|
||||
|
||||
//Set missing real_start_time and/or real_end_time for all position_instances to the real_start_time/real_end_time of their related instance
|
||||
match crate::database::controller::events::instances::instance_positions::set_missing_real_times_to_instance_times(settings, event_id) {
|
||||
Ok(_) => (),
|
||||
Err(_) => {
|
||||
return Err(Json(ApiError::new(500, "Couldn't set missing real times to planned times for position_instances.".to_string()).to_wrapper()));
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: remove empty position instances
|
||||
match crate::database::controller::events::approve_event_times(settings, event_id) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[put("/api/events/<entity_id>/finish_personnel_billing", format = "json")]
|
||||
pub fn finish_personnel_billing(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
entity_id: String,
|
||||
) -> Result<(), Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
|
||||
let event_id = parse_uuid_string(entity_id)?;
|
||||
|
||||
let event = match get_event(settings, event_id) {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
warn!("{} tried to close event with invalid event_id {}: {}", caller.entity_id, event_id, e);
|
||||
return Err(Json(ApiError::new(404, "No event with this entity_id found.".to_string()).to_wrapper()));
|
||||
}
|
||||
};
|
||||
|
||||
if !caller.has_permission(crate::permissions::modules::event_billing::personnel::EDIT.to_string()) {
|
||||
match event.member_responsible {
|
||||
Some(resp) => {
|
||||
if caller.entity_id != resp {
|
||||
warn!("No permissions to edit event billing for member {}", caller.entity_id);
|
||||
return Err(Json(ApiError::new(403, "No permissions to edit event billing.".to_string()).to_wrapper()));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("No permissions to edit event billing for member {}", caller.entity_id);
|
||||
return Err(Json(ApiError::new(403, "No permissions to edit event billing.".to_string()).to_wrapper()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match crate::database::controller::events::finish_personnel_billing(settings, event_id) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/api/events/<entity_id>/approve?<stage>", format = "json")]
|
||||
pub async fn approve(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
entity_id: String,
|
||||
stage: String,
|
||||
mq: &State<Arc<MailQueue>>,
|
||||
) -> Result<(), Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
|
||||
let event_id = parse_uuid_string(entity_id)?;
|
||||
|
||||
let stage = parse_uuid_string(stage)?;
|
||||
let billing_state = match get_billing_state(settings, stage) {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
warn!("Couldn't find stage: {}", e);
|
||||
return Err(Json(ApiError::new(404, "Stage not found.".to_string()).to_wrapper()));
|
||||
}
|
||||
};
|
||||
|
||||
if !check_access_to_resource(settings, caller.entity_id, stage, crate::permissions::modules::event_billing::APPROVE) {
|
||||
return Err(Json(ApiError::new(403, "No permission to approve this billing state.".to_string()).to_wrapper()));
|
||||
}
|
||||
|
||||
match crate::database::controller::events::approve_stage(settings, event_id, stage, caller.entity_id) {
|
||||
Ok(_) => {
|
||||
if billing_state.final_approve {
|
||||
if let Err(e) = finish_billing(settings, event_id) {
|
||||
return Err(translate_diesel(e));
|
||||
}
|
||||
let csv = match generate_billing_csv(settings, event_id) {
|
||||
Ok(csv) => csv,
|
||||
Err(e) => {
|
||||
error!("Error generating CSV: {}", e); //TODO: report failure to user
|
||||
return Ok(())
|
||||
}
|
||||
};
|
||||
debug!("Generated CSV: {}", csv);
|
||||
if settings.billing.send_personnel_billing_to_email {
|
||||
let attachement = Attachment::new(String::from("Einsatzabrechnung.csv")).body(csv, ContentType::parse("text/csv").unwrap());
|
||||
let msg = Message::builder()
|
||||
.from(settings.mail.from.clone().parse().unwrap())
|
||||
.reply_to(settings.mail.reply_to.clone().parse().unwrap())
|
||||
.to(settings.billing.personnel_billing_email.parse().unwrap())
|
||||
.subject("Einsatzabrechnung")
|
||||
.multipart(MultiPart::mixed().singlepart(attachement).singlepart(SinglePart::plain(String::from("Es wurde eine Einsatzabrechnung freigegeben. Sie befindet sich im Anhang dieser E-Mail.")))).unwrap();
|
||||
mq.add_mail(msg);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => Err(translate_diesel(e))
|
||||
}
|
||||
}
|
|
@ -10,4 +10,7 @@ pub mod appointments;
|
|||
pub mod info;
|
||||
pub mod event_organisers;
|
||||
pub mod events;
|
||||
pub mod permissions;
|
||||
pub mod permissions;
|
||||
pub mod billing_states;
|
||||
pub mod billing_rates;
|
||||
pub mod personnel_billing;
|
|
@ -0,0 +1,56 @@
|
|||
use bigdecimal::BigDecimal;
|
||||
use chrono::Utc;
|
||||
|
||||
pub fn calculate_hours(real_start_time: chrono::DateTime<Utc>, real_end_time: chrono::DateTime<Utc>) -> Option<i32> {
|
||||
let timestamp_start = real_start_time.timestamp() as f64;
|
||||
let timestamp_end = real_end_time.timestamp() as f64;
|
||||
|
||||
if timestamp_start == timestamp_end {
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
let hours_since_start = (timestamp_start / 3600.0).floor();
|
||||
let hours_since_end = (timestamp_end / 3600.0).ceil();
|
||||
|
||||
if hours_since_start > hours_since_end {
|
||||
error!("real_start_time is after real_end_time");
|
||||
None
|
||||
} else {
|
||||
Some((hours_since_end - hours_since_start) as i32)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{TimeZone, Utc};
|
||||
|
||||
use crate::modules::api::personnel_billing::calculation::calculate_hours;
|
||||
|
||||
#[test]
|
||||
fn start_after_end() {
|
||||
let start = Utc.ymd(2022, 2, 2).and_hms_milli(0, 0, 1, 444);
|
||||
let end = Utc.ymd(2022, 1, 1).and_hms_milli(0, 0, 1, 444);
|
||||
assert!(calculate_hours(start, end).is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_eq_end() {
|
||||
let start = Utc.ymd(2022, 2, 2).and_hms_milli(0, 0, 1, 444);
|
||||
let end = Utc.ymd(2022, 2, 2).and_hms_milli(0, 0, 1, 444);
|
||||
assert_eq!(0, calculate_hours(start, end).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minutes_eq() {
|
||||
let start = Utc.ymd(2022, 2, 2).and_hms_milli(14, 15, 1, 444);
|
||||
let end = Utc.ymd(2022, 2, 2).and_hms_milli(16, 15, 1, 444);
|
||||
assert_eq!(3, calculate_hours(start, end).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_full_hour() {
|
||||
let start = Utc.ymd(2022, 2, 2).and_hms_milli(14, 15, 1, 444);
|
||||
let end = Utc.ymd(2022, 2, 2).and_hms_milli(14, 45, 1, 444);
|
||||
assert_eq!(1, calculate_hours(start, end).unwrap())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
use bigdecimal::BigDecimal;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::billing::personnel_billing::RawPersonnelBilling;
|
||||
use crate::database::controller::billing::personnel_billing_rates::{get_billing_rate, get_billing_rates};
|
||||
use crate::database::controller::events::{get_position_instance, get_position_instances};
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
use crate::modules::api::personnel_billing::calculation::calculate_hours;
|
||||
use crate::Settings;
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize)]
|
||||
pub struct AddPersonnelBillingEntryData {
|
||||
pub(crate) position_instance_id: uuid::Uuid,
|
||||
pub(crate) billing_rate_id: Option<uuid::Uuid>,
|
||||
pub(crate) money_from_lump_sum: Option<String>,
|
||||
pub(crate) money_for_time: Option<String>,
|
||||
}
|
||||
|
||||
#[post(
|
||||
"/api/personnel_billing",
|
||||
format = "json", data = "<data>"
|
||||
)]
|
||||
pub fn add_personnel_billing_entry(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
data: Json<AddPersonnelBillingEntryData>,
|
||||
) -> Result<(), Json<ApiErrorWrapper>> { //TODO: return entry
|
||||
let caller = parse_member_cookie(cookie.member.clone())?;
|
||||
//TODO: allow member_responsible for event to bypass this permission
|
||||
if !caller
|
||||
.has_permission(crate::permissions::modules::event_billing::personnel::EDIT.to_string())
|
||||
{
|
||||
return Err(Json(
|
||||
ApiError::new(
|
||||
403,
|
||||
"Keine Berechtigung Einsatzabrechnung zu bearbeiten!".to_string(),
|
||||
)
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
|
||||
let data = data.into_inner();
|
||||
|
||||
if (data.money_from_lump_sum.is_none() || data.money_for_time.is_none()) && data.billing_rate_id.is_none() {
|
||||
return Err(Json(
|
||||
ApiError::new(
|
||||
400,
|
||||
"Missing either money_from_lump_sum+money_for_time or billing_rate_id.".to_string(),
|
||||
)
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
|
||||
let position_instance = match get_position_instance(settings, data.position_instance_id) {
|
||||
Ok(res) => res,
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
};
|
||||
|
||||
let fulfilled_time = if let Some(start) = position_instance.real_start_time {
|
||||
if let Some(end) = position_instance.real_end_time {
|
||||
calculate_hours(start, end)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let fulfilled_time = match fulfilled_time {
|
||||
Some(ft) => ft,
|
||||
None => {
|
||||
error!("Couldn't calculate fulfilled time for position_instance {}!", position_instance.position_instance_id);
|
||||
return Err(Json(
|
||||
ApiError::new(
|
||||
500,
|
||||
"Couldn't calculate fulfilled time.".to_string(),
|
||||
)
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let money_for_time: BigDecimal;
|
||||
let money_from_lump_sum: BigDecimal;
|
||||
|
||||
match data.billing_rate_id {
|
||||
Some(bri) => {
|
||||
let billing_rate = match get_billing_rate(settings, bri) {
|
||||
Ok(res) => res,
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
};
|
||||
money_for_time = billing_rate.payment_per_hour.clone() * BigDecimal::from(fulfilled_time);
|
||||
money_from_lump_sum = billing_rate.lump_sum.clone();
|
||||
}
|
||||
None => {
|
||||
match data.money_for_time {
|
||||
Some(mft) => {
|
||||
match data.money_from_lump_sum {
|
||||
Some(mfls) => {
|
||||
money_for_time = BigDecimal::from(mft.parse::<f32>().unwrap()); //TODO: do not unwrap
|
||||
money_from_lump_sum = BigDecimal::from(mfls.parse::<f32>().unwrap()); //TODO: do not unwrap
|
||||
}
|
||||
None => return Err(Json(
|
||||
ApiError::new(
|
||||
400,
|
||||
"Can't create personnel billing entry for positon_instance with billable = false!".to_string(),
|
||||
)
|
||||
.to_wrapper(),
|
||||
))
|
||||
}
|
||||
}
|
||||
None => return Err(Json(
|
||||
ApiError::new(
|
||||
400,
|
||||
"Can't create personnel billing entry for positon_instance with billable = false!".to_string(),
|
||||
)
|
||||
.to_wrapper(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total_money = money_from_lump_sum.clone() + money_for_time.clone();
|
||||
|
||||
let new_entry = RawPersonnelBilling {
|
||||
position_instance_id: data.position_instance_id,
|
||||
member_id: position_instance.taken_by.unwrap(),
|
||||
fulfilled_time,
|
||||
money_for_time,
|
||||
money_from_lump_sum,
|
||||
total_money,
|
||||
};
|
||||
match crate::database::controller::billing::personnel_billing::create(settings, new_entry) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(translate_diesel(e))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
use crate::Settings;
|
||||
|
||||
#[delete("/api/personnel_billing/<position_instance_id>", format = "json")]
|
||||
pub fn delete_position_instance(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
position_instance_id: String,
|
||||
) -> Result<(), Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
if !caller.has_permission(crate::permissions::modules::event_billing::personnel::EDIT.to_string()) {
|
||||
return Err(Json(
|
||||
ApiError::new(403, "Keine Berechtigung Abrechnung zu bearbeiten!".to_string()).to_wrapper(),
|
||||
));
|
||||
}
|
||||
|
||||
match crate::database::controller::billing::personnel_billing::remove(settings, parse_uuid_string(position_instance_id)?) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
pub mod read;
|
||||
pub mod delete;
|
||||
pub mod create;
|
||||
pub mod calculation;
|
|
@ -0,0 +1,63 @@
|
|||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::modules::api::events::instances::read::{EventUnitInstance, PositionInstance};
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
|
||||
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
|
||||
use crate::Settings;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PersonnelBilling {
|
||||
pub(crate) position_instance_id: uuid::Uuid,
|
||||
pub(crate) member_id: uuid::Uuid,
|
||||
pub(crate) fulfilled_time: i32,
|
||||
pub(crate) money_for_time: String,
|
||||
pub(crate) money_from_lump_sum: String,
|
||||
pub(crate) total_money: String,
|
||||
}
|
||||
|
||||
#[get(
|
||||
"/api/personnel_billing/<position_instance_id>",
|
||||
format = "json"
|
||||
)]
|
||||
pub fn get_personnel_billing(
|
||||
settings: &State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
position_instance_id: String,
|
||||
) -> Result<Json<Option<PersonnelBilling>>, Json<ApiErrorWrapper>> {
|
||||
let caller = parse_member_cookie(cookie.member.clone())?;
|
||||
//TODO: allow member_responsible for event to bypass this permission
|
||||
if !caller
|
||||
.has_permission(crate::permissions::modules::event_billing::personnel::VIEW.to_string())
|
||||
{
|
||||
return Err(Json(
|
||||
ApiError::new(
|
||||
403,
|
||||
"Keine Berechtigung Einsatzabrechnung anzusehen!".to_string(),
|
||||
)
|
||||
.to_wrapper(),
|
||||
));
|
||||
}
|
||||
|
||||
let pid = parse_uuid_string(position_instance_id)?;
|
||||
match crate::database::controller::billing::personnel_billing::read(settings, pid) {
|
||||
Ok(res) => {
|
||||
Ok(Json(match res {
|
||||
Some(res) => {
|
||||
Some(PersonnelBilling {
|
||||
position_instance_id: res.position_instance_id,
|
||||
member_id: res.member_id,
|
||||
fulfilled_time: res.fulfilled_time,
|
||||
money_for_time: res.money_for_time.to_string(),
|
||||
money_from_lump_sum: res.money_from_lump_sum.to_string(),
|
||||
total_money: res.total_money.to_string(),
|
||||
})
|
||||
},
|
||||
None => None
|
||||
}))
|
||||
},
|
||||
Err(e) => Err(translate_diesel(e))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
use rocket::http::Status;
|
||||
use rocket::State;
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::database::controller::events::get_event;
|
||||
use crate::database::controller::members::check_access_to_resource;
|
||||
use crate::database::model::events::Event;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::sitebuilder::model::general::{Footer, Header, Script, Stylesheet};
|
||||
use crate::helper::sitebuilder::model::sidebar::Sidebar;
|
||||
use crate::modules::api::member_management::controller::parser::parse_uuid_string;
|
||||
use crate::modules::event_billing::event::EventBilling;
|
||||
use crate::Settings;
|
||||
|
||||
/// UI for viewing, editing & approving event unit times.
|
||||
/// required permissions:
|
||||
/// * modules.event_billing.start_end_times.view OR be member_responsible for event to view real start/end times
|
||||
/// * modules.event_billing.start_end_times.edit OR be member_responsible for event to edit real start/end times
|
||||
#[get("/portal/eb/approve?<event_id>&<stage>")]
|
||||
pub fn approve(cookie: SessionCookie, settings: &State<Settings>, event_id: String, stage: String) -> Result<Template, Status> {
|
||||
let caller = match cookie.member {
|
||||
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
|
||||
Some(caller) => caller,
|
||||
None => return Err(Status::Unauthorized),
|
||||
};
|
||||
|
||||
match stage.parse() {
|
||||
Ok(stage) => {
|
||||
if !check_access_to_resource(settings, caller.entity_id, stage, crate::permissions::modules::event_billing::APPROVE) {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Unparsable stage in approve: {}", e);
|
||||
return Err(Status::BadRequest);
|
||||
},
|
||||
}
|
||||
|
||||
let event_id: uuid::Uuid = match event_id.parse() {
|
||||
Ok(event_id) => event_id,
|
||||
Err(e) => {
|
||||
warn!("Unparsable event_id in approve: {}", e);
|
||||
return Err(Status::BadRequest);
|
||||
}
|
||||
};
|
||||
|
||||
let event = match get_event(settings, event_id) {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
warn!("Couldn't load event: {}", e);
|
||||
return Err(Status::NotFound);
|
||||
}
|
||||
};
|
||||
|
||||
//Do not allow editing times before event is closed
|
||||
if event.state < 7 {
|
||||
error!("User tried to approve before event was billed");
|
||||
return Err(Status::BadRequest);
|
||||
}
|
||||
|
||||
let header = Header {
|
||||
html_language: "de".to_string(),
|
||||
site_title: "Einsatzabrechnung".to_string(),
|
||||
stylesheets: vec![Stylesheet {
|
||||
path: "/css/errms.css".to_string(),
|
||||
}],
|
||||
};
|
||||
let footer = Footer {
|
||||
scripts: vec![Script {
|
||||
path: "/js/event_billing.js".to_string(),
|
||||
}, Script {
|
||||
path: "/js/mini_searchbar.js".to_string(),
|
||||
}],
|
||||
};
|
||||
let mut sidebar = Sidebar::new(caller.clone());
|
||||
sidebar.event_billing.active = true;
|
||||
|
||||
Ok(Template::render("module_eb_approve", EventBilling {
|
||||
header,
|
||||
footer,
|
||||
sidebar,
|
||||
caller: caller.entity_id,
|
||||
event,
|
||||
}))
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
use rocket::http::Status;
|
||||
use rocket::State;
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::database::controller::events::get_event;
|
||||
use crate::database::model::events::Event;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::sitebuilder::model::general::{Footer, Header, Script, Stylesheet};
|
||||
use crate::helper::sitebuilder::model::sidebar::Sidebar;
|
||||
use crate::modules::event_billing::event::EventBilling;
|
||||
use crate::Settings;
|
||||
|
||||
/// Close Event UI
|
||||
/// required permissions:
|
||||
/// * modules.event_management.events.edit OR be member_responsible for event
|
||||
#[get("/portal/eb/close_event?<event_id>")]
|
||||
pub fn close_event(cookie: SessionCookie, settings: &State<Settings>, event_id: String) -> Result<Template, Status> {
|
||||
let caller = match cookie.member {
|
||||
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
|
||||
Some(caller) => caller,
|
||||
None => return Err(Status::Unauthorized),
|
||||
};
|
||||
|
||||
let event_id: uuid::Uuid = match event_id.parse() {
|
||||
Ok(event_id) => event_id,
|
||||
Err(e) => {
|
||||
warn!("Unparsable event_id in close_event: {}", e);
|
||||
return Err(Status::BadRequest);
|
||||
}
|
||||
};
|
||||
|
||||
let event = match get_event(settings, event_id) {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
warn!("Couldn't load event: {}", e);
|
||||
return Err(Status::NotFound);
|
||||
}
|
||||
};
|
||||
|
||||
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
|
||||
match event.member_responsible {
|
||||
Some(resp) => {
|
||||
if caller.entity_id != resp {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let header = Header {
|
||||
html_language: "de".to_string(),
|
||||
site_title: "Einsatzabrechnung".to_string(),
|
||||
stylesheets: vec![Stylesheet {
|
||||
path: "/css/errms.css".to_string(),
|
||||
}],
|
||||
};
|
||||
let footer = Footer {
|
||||
scripts: vec![Script {
|
||||
path: "/js/event_billing.js".to_string(),
|
||||
}],
|
||||
};
|
||||
let mut sidebar = Sidebar::new(caller.clone());
|
||||
sidebar.event_billing.active = true;
|
||||
|
||||
Ok(Template::render("module_eb_close_event", EventBilling {
|
||||
header,
|
||||
footer,
|
||||
sidebar,
|
||||
caller: caller.entity_id,
|
||||
event,
|
||||
}))
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
use rocket::http::Status;
|
||||
use rocket::State;
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::database::controller::events::get_event;
|
||||
use crate::database::model::events::Event;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::sitebuilder::model::general::{Footer, Header, Script, Stylesheet};
|
||||
use crate::helper::sitebuilder::model::sidebar::Sidebar;
|
||||
use crate::modules::event_billing::event::EventBilling;
|
||||
use crate::Settings;
|
||||
|
||||
/// UI for viewing, editing & approving personnel billing for an event.
|
||||
/// required permissions:
|
||||
/// * modules.event_billing.personnel.view OR be member_responsible for event to view personnel billing details
|
||||
/// * modules.event_billing.personnel.edit OR be member_responsible for event to edit personnel billing details
|
||||
#[get("/portal/eb/personnel_billing?<event_id>")]
|
||||
pub fn edit_personnel_billing(cookie: SessionCookie, settings: &State<Settings>, event_id: String) -> Result<Template, Status> {
|
||||
let caller = match cookie.member {
|
||||
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
|
||||
Some(caller) => caller,
|
||||
None => return Err(Status::Unauthorized),
|
||||
};
|
||||
|
||||
let event_id: uuid::Uuid = match event_id.parse() {
|
||||
Ok(event_id) => event_id,
|
||||
Err(e) => {
|
||||
warn!("Unparsable event_id in personnel_billing: {}", e);
|
||||
return Err(Status::BadRequest);
|
||||
}
|
||||
};
|
||||
|
||||
let event = match get_event(settings, event_id) {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
warn!("Couldn't load event: {}", e);
|
||||
return Err(Status::NotFound);
|
||||
}
|
||||
};
|
||||
|
||||
//Do not allow editing times before times are submitted
|
||||
if event.state < 6 {
|
||||
error!("User tried to edit personnel billing before times were submitted");
|
||||
return Err(Status::BadRequest);
|
||||
}
|
||||
//TODO: restrict "going-back" to members with specific permission (https://git.anghenfil.de/EinsatzOnline/EinsatzOnline/issues/21)
|
||||
|
||||
if !caller.has_permission(crate::permissions::modules::event_billing::personnel::VIEW.to_string()) {
|
||||
match event.member_responsible {
|
||||
Some(resp) => {
|
||||
if caller.entity_id != resp {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let header = Header {
|
||||
html_language: "de".to_string(),
|
||||
site_title: "Einsatzabrechnung".to_string(),
|
||||
stylesheets: vec![Stylesheet {
|
||||
path: "/css/errms.css".to_string(),
|
||||
}],
|
||||
};
|
||||
let footer = Footer {
|
||||
scripts: vec![Script {
|
||||
path: "/js/event_billing.js".to_string(),
|
||||
}, Script {
|
||||
path: "/js/search2.js".to_string(),
|
||||
}],
|
||||
};
|
||||
let mut sidebar = Sidebar::new(caller.clone());
|
||||
sidebar.event_billing.active = true;
|
||||
|
||||
Ok(Template::render("module_eb_personnel_billing", EventBilling {
|
||||
header,
|
||||
footer,
|
||||
sidebar,
|
||||
caller: caller.entity_id,
|
||||
event,
|
||||
}))
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
use rocket::http::Status;
|
||||
use rocket::State;
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::database::controller::events::get_event;
|
||||
use crate::database::model::events::Event;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::sitebuilder::model::general::{Footer, Header, Script, Stylesheet};
|
||||
use crate::helper::sitebuilder::model::sidebar::Sidebar;
|
||||
use crate::modules::event_billing::event::EventBilling;
|
||||
use crate::Settings;
|
||||
|
||||
/// UI for viewing, editing & approving event unit times.
|
||||
/// required permissions:
|
||||
/// * modules.event_billing.start_end_times.view OR be member_responsible for event to view real start/end times
|
||||
/// * modules.event_billing.start_end_times.edit OR be member_responsible for event to edit real start/end times
|
||||
#[get("/portal/eb/edit_times?<event_id>")]
|
||||
pub fn edit_times(cookie: SessionCookie, settings: &State<Settings>, event_id: String) -> Result<Template, Status> {
|
||||
let caller = match cookie.member {
|
||||
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
|
||||
Some(caller) => caller,
|
||||
None => return Err(Status::Unauthorized),
|
||||
};
|
||||
|
||||
let event_id: uuid::Uuid = match event_id.parse() {
|
||||
Ok(event_id) => event_id,
|
||||
Err(e) => {
|
||||
warn!("Unparsable event_id in edit_times: {}", e);
|
||||
return Err(Status::BadRequest);
|
||||
}
|
||||
};
|
||||
|
||||
let event = match get_event(settings, event_id) {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
warn!("Couldn't load event: {}", e);
|
||||
return Err(Status::NotFound);
|
||||
}
|
||||
};
|
||||
|
||||
//Do not allow editing times before event is closed
|
||||
if event.state < 4 {
|
||||
error!("User tried to edit times before event is closed");
|
||||
return Err(Status::BadRequest);
|
||||
}
|
||||
//TODO: restrict "going-back" to members with specific permission (https://git.anghenfil.de/EinsatzOnline/EinsatzOnline/issues/21)
|
||||
|
||||
if !caller.has_permission(crate::permissions::modules::event_billing::start_end_times::VIEW.to_string()) {
|
||||
match event.member_responsible {
|
||||
Some(resp) => {
|
||||
if caller.entity_id != resp {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let header = Header {
|
||||
html_language: "de".to_string(),
|
||||
site_title: "Einsatzabrechnung".to_string(),
|
||||
stylesheets: vec![Stylesheet {
|
||||
path: "/css/errms.css".to_string(),
|
||||
}],
|
||||
};
|
||||
let footer = Footer {
|
||||
scripts: vec![Script {
|
||||
path: "/js/event_billing.js".to_string(),
|
||||
}, Script {
|
||||
path: "/js/search2.js".to_string(),
|
||||
}],
|
||||
};
|
||||
let mut sidebar = Sidebar::new(caller.clone());
|
||||
sidebar.event_billing.active = true;
|
||||
|
||||
Ok(Template::render("module_eb_edit_times", EventBilling {
|
||||
header,
|
||||
footer,
|
||||
sidebar,
|
||||
caller: caller.entity_id,
|
||||
event,
|
||||
}))
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
use rocket::http::Status;
|
||||
use rocket::State;
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::database::controller::events::get_event;
|
||||
use crate::database::model::events::Event;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::sitebuilder::model::general::{Footer, Header, Script, Stylesheet};
|
||||
use crate::helper::sitebuilder::model::sidebar::Sidebar;
|
||||
use crate::Settings;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EventBilling {
|
||||
pub header: Header,
|
||||
pub footer: Footer,
|
||||
pub sidebar: Sidebar,
|
||||
pub caller: uuid::Uuid,
|
||||
pub event: Event,
|
||||
}
|
||||
|
||||
|
||||
#[get("/portal/eb/event?<event_id>")]
|
||||
pub fn event_view(cookie: SessionCookie, settings: &State<Settings>, event_id: String) -> Result<Template, Status> {
|
||||
let caller = match cookie.member {
|
||||
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
|
||||
Some(caller) => caller,
|
||||
None => return Err(Status::Unauthorized),
|
||||
};
|
||||
|
||||
let event_id: uuid::Uuid = match event_id.parse() {
|
||||
Ok(event_id) => event_id,
|
||||
Err(e) => {
|
||||
warn!("Unparsable event_id in event_billing_event_view: {}", e);
|
||||
return Err(Status::BadRequest);
|
||||
}
|
||||
};
|
||||
|
||||
let event = match get_event(settings, event_id) {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
warn!("Couldn't load event: {}", e);
|
||||
return Err(Status::NotFound);
|
||||
}
|
||||
};
|
||||
|
||||
if !caller.has_permission(crate::permissions::modules::event_billing::VIEW.to_string()) {
|
||||
match event.member_responsible {
|
||||
Some(resp) => {
|
||||
if caller.entity_id != resp {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(Status::Forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let header = Header {
|
||||
html_language: "de".to_string(),
|
||||
site_title: "Einsatzabrechnung".to_string(),
|
||||
stylesheets: vec![Stylesheet {
|
||||
path: "/css/errms.css".to_string(),
|
||||
}],
|
||||
};
|
||||
let footer = Footer {
|
||||
scripts: vec![Script {
|
||||
path: "/js/event_billing.js".to_string(),
|
||||
}],
|
||||
};
|
||||
let mut sidebar = Sidebar::new(caller.clone());
|
||||
sidebar.event_billing.active = true;
|
||||
|
||||
Ok(Template::render("module_eb_event_view", EventBilling {
|
||||
header,
|
||||
footer,
|
||||
sidebar,
|
||||
caller: caller.entity_id,
|
||||
event,
|
||||
}))
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::billing::personnel_billing_rates::get_billing_rate;
|
||||
use crate::database::controller::events::{get_event, get_position_instance, get_position_instances};
|
||||
use crate::database::controller::events::instances::instances::get_instances;
|
||||
use crate::database::controller::groups::get_group;
|
||||
use crate::database::controller::members::get_member_by_uuid;
|
||||
use crate::helper::time::utc_to_local_user_time;
|
||||
use crate::modules::api::personnel_billing::read::get_personnel_billing;
|
||||
use crate::Settings;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum CSVGeneratorErrorKind {
|
||||
MissingTimes,
|
||||
MissingMember,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CSVGeneratorError {
|
||||
Database(diesel::result::Error),
|
||||
Generator(CSVGeneratorErrorKind),
|
||||
IoError(std::io::Error),
|
||||
}
|
||||
|
||||
impl CSVGeneratorErrorKind {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match *self {
|
||||
CSVGeneratorErrorKind::MissingTimes => "Missing real start and/or real end times.",
|
||||
CSVGeneratorErrorKind::MissingMember => "Missing member details. Maybe member got removed?",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CSVGeneratorError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
CSVGeneratorError::Generator(ref err) => write!(f, "CSV Generator error: {:?}", err),
|
||||
CSVGeneratorError::Database(ref err) => std::fmt::Display::fmt(&err, f),
|
||||
CSVGeneratorError::IoError(ref err) => std::fmt::Display::fmt(&err, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diesel::result::Error> for CSVGeneratorError {
|
||||
fn from(err: diesel::result::Error) -> CSVGeneratorError {
|
||||
CSVGeneratorError::Database(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for CSVGeneratorError {
|
||||
fn from(err: std::io::Error) -> CSVGeneratorError {
|
||||
CSVGeneratorError::IoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_billing_csv(settings: &State<Settings>, csv: String, event_name: String) -> Result<String, std::io::Error> {
|
||||
let mut path_draft = format!("data/personnel_billing_csv/csv_{}", event_name);
|
||||
let mut path = Path::new(&path_draft);
|
||||
|
||||
if path.exists() {
|
||||
let mut i = 1;
|
||||
loop {
|
||||
path_draft = format!("data/personnel_billing_csv/csv_{}_{}", event_name, i);
|
||||
if !Path::new(&path_draft).exists() {
|
||||
break;
|
||||
} else {
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
path = Path::new(&path_draft);
|
||||
}
|
||||
|
||||
let mut file = File::create(&path)?;
|
||||
file.write_all(csv.as_bytes())?;
|
||||
Ok(path_draft)
|
||||
}
|
||||
|
||||
pub fn generate_billing_csv(settings: &State<Settings>, event_id: uuid::Uuid) -> Result<String, CSVGeneratorError> {
|
||||
let mut res = String::new();
|
||||
|
||||
let event_data = get_event(settings, event_id)?;
|
||||
res.push_str(&format!("{}, {}\n", sanitize(String::from("Einsatz: ")), sanitize(event_data.name.clone())));
|
||||
|
||||
if let Some(related_group) = event_data.related_group {
|
||||
let group = get_group(settings, related_group)?;
|
||||
res.push_str(&format!("{}, {}\n", sanitize(String::from("Gruppe:")), sanitize(group.name)));
|
||||
}
|
||||
|
||||
if let Some(member_responsible) = event_data.member_responsible {
|
||||
if let Some(member) = get_member_by_uuid(member_responsible, settings) {
|
||||
res.push_str(&format!("{}, {}\n", sanitize(String::from("Verantwortliches Mitglied:")), sanitize(format!("{} {}", member.firstname, member.lastname))))
|
||||
}
|
||||
}
|
||||
|
||||
res.push_str("\n");
|
||||
|
||||
let instances = get_instances(settings, event_id)?;
|
||||
|
||||
for instance in instances {
|
||||
res.push_str(&format!("Einheit:, {}\n", sanitize(instance.name)));
|
||||
if let Some(billing_rate_id) = instance.billing_rate_id {
|
||||
let billing_rate = get_billing_rate(settings, billing_rate_id)?;
|
||||
|
||||
res.push_str(&format!("Abrechnungssatz:, {}, {}, Pauschale:, {}, €/Stunde:, {}\n", sanitize(billing_rate.name), sanitize(billing_rate.description.unwrap_or(String::from(""))), sanitize(format!("{} €", billing_rate.lump_sum)), sanitize(format!("{} €", billing_rate.payment_per_hour))));
|
||||
}
|
||||
|
||||
res.push_str(&format!("{}, {}, {}, {}, {}, {}, {}, {}, {}\n", String::from("Personalnummer"), String::from("Vorname"), String::from("Nachname"), String::from("Von"), String::from("Bis"), String::from("Stunden"), String::from("Pauschale"), String::from("Stundengeld"), String::from("Gesamt")));
|
||||
|
||||
|
||||
let position_instances = get_position_instances(settings, instance.instance_id)?;
|
||||
for position_instance in position_instances {
|
||||
if let Some(personnel) = crate::database::controller::billing::personnel_billing::read(settings, position_instance.position_instance_id)? {
|
||||
match get_member_by_uuid(personnel.member_id, settings) {
|
||||
Some(member) => {
|
||||
let begin = match position_instance.real_start_time {
|
||||
Some(start) => utc_to_local_user_time(settings, None, start),
|
||||
None => return Err(CSVGeneratorError::Generator(CSVGeneratorErrorKind::MissingTimes))
|
||||
};
|
||||
let end = match position_instance.real_end_time {
|
||||
Some(end) => utc_to_local_user_time(settings, None, end),
|
||||
None => return Err(CSVGeneratorError::Generator(CSVGeneratorErrorKind::MissingTimes))
|
||||
};
|
||||
|
||||
|
||||
res.push_str(&format!("{}, {}, {}, {}, {}, {}, {}, {}, {}\n", sanitize(member.personnel_number.unwrap_or(0).to_string()), sanitize(member.firstname), sanitize(member.lastname), sanitize(begin), sanitize(end), sanitize(personnel.fulfilled_time.to_string()), sanitize(personnel.money_from_lump_sum.to_string()), sanitize(personnel.money_for_time.to_string()), sanitize(personnel.total_money.to_string())));
|
||||
}
|
||||
None => {
|
||||
return Err(CSVGeneratorError::Generator(CSVGeneratorErrorKind::MissingMember));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
res.push_str("\n");
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn sanitize(mut input: String) -> String {
|
||||
input = input.replace("\"", "\"\"") //escape double quotes by another double quote
|
||||
.replace("\r", "") //Remove carriage returns
|
||||
.replace("\n", "") //Remove new lines
|
||||
.replace("\t", ""); //Remove tabs
|
||||
if input.starts_with("@") || input.starts_with("=") || input.starts_with("+") || input.starts_with("-") { //Add single quote if input starts with @ = + or -
|
||||
input = format!("\'{}", input);
|
||||
}
|
||||
|
||||
format!("\"{}\"", input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::modules::event_billing::generate_billing_csv::sanitize;
|
||||
|
||||
#[test]
|
||||
fn csv_injection_test1() {
|
||||
assert_eq!(sanitize(String::from("Tes\"t123")), String::from("\"Tes\"\"t123\""))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_injection_test2() {
|
||||
assert_eq!(sanitize(String::from("=1+2\";=1+2")), String::from("\"'=1+2\"\";=1+2\""))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_injection_test3() {
|
||||
assert_eq!(sanitize(String::from("=SUM(A1, A2)")), String::from("\"'=SUM(A1, A2)\""))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
pub mod event;
|
||||
pub mod close_event;
|
||||
pub mod edit_times;
|
||||
pub mod approve;
|
||||
pub mod edit_personnel_billing;
|
||||
pub mod generate_billing_csv;
|
|
@ -45,6 +45,9 @@ pub fn edit_event(cookie: SessionCookie, _settings: &State<Settings>, id: String
|
|||
},
|
||||
Script {
|
||||
path: "/js/em_edit_event.js".to_string(),
|
||||
},
|
||||
Script {
|
||||
path: "/js/search2.js".to_string(),
|
||||
}],
|
||||
};
|
||||
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
use crate::helper::settings::Settings;
|
||||
use rocket::State;
|
||||
use crate::database::controller::users::get_user_by_email;
|
||||
use crate::database::controller::password_resets::add_token;
|
||||
use crate::helper::mail_templates::MailTemplates;
|
||||
use crate::helper::mail_queue::queue::{Mail, MailQueue};
|
||||
use std::sync::Arc;
|
||||
|
||||
use lettre::Message;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::password_resets::add_token;
|
||||
use crate::database::controller::users::get_user_by_email;
|
||||
use crate::helper::mail_queue::queue::MailQueue;
|
||||
use crate::helper::mail_templates::MailTemplates;
|
||||
use crate::helper::settings::Settings;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PasswortResetMail{
|
||||
pub struct PasswortResetMail {
|
||||
frontpage: String,
|
||||
email: String,
|
||||
reset_url: String,
|
||||
|
@ -24,18 +27,32 @@ pub fn request_password_reset(settings: &State<Settings>, mt: &State<MailTemplat
|
|||
Ok(token) => token,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
let pwrm = PasswortResetMail{
|
||||
let pwrm = PasswortResetMail {
|
||||
frontpage: settings.application.url.clone(),
|
||||
email: email.clone(),
|
||||
reset_url: format!("{}password_reset?token={}", settings.application.url.clone(), token)
|
||||
reset_url: format!("{}password_reset?token={}", settings.application.url.clone(), token),
|
||||
};
|
||||
let body = match mt.registry.render("password-reset-de", &pwrm){
|
||||
let body = match mt.registry.render("password-reset-de", &pwrm) {
|
||||
Ok(body) => body,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
let mail = Mail::new(settings.mail.from.clone(), vec![email], format!("[{}] - Passwort Zuruecksetzen", settings.application.name.clone()), vec![], vec![], Some(settings.mail.reply_to.clone()), body, None); //TODO: Add deliver_until
|
||||
|
||||
match mq.add_mail(mail){
|
||||
let email_address = match email.parse() {
|
||||
Ok(email) => email,
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse password reset email: {}", e);
|
||||
return Err(())
|
||||
}
|
||||
};
|
||||
|
||||
let mail = Message::builder()
|
||||
.from(settings.mail.from.clone().parse().unwrap())
|
||||
.reply_to(settings.mail.reply_to.clone().parse().unwrap())
|
||||
.to(email_address)
|
||||
.subject("Einsatz Online - Passwort Zurücksetzen") //TODO: use application name setting
|
||||
.body(body).unwrap();
|
||||
|
||||
match mq.add_mail(mail) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(())
|
||||
}
|
||||
|
|
|
@ -51,10 +51,12 @@ table! {
|
|||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
billing_states (state_id) {
|
||||
state_id -> Text,
|
||||
billing_states (entity_id) {
|
||||
name -> Text,
|
||||
description -> Nullable<Text>,
|
||||
final_approve -> Bool,
|
||||
order -> Nullable<Int4>,
|
||||
entity_id -> Uuid,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,7 +137,8 @@ table! {
|
|||
real_start_time -> Nullable<Timestamptz>,
|
||||
real_end_time -> Nullable<Timestamptz>,
|
||||
billing_rate_id -> Nullable<Uuid>,
|
||||
billing_state_id -> Nullable<Text>,
|
||||
billing_state_id -> Nullable<Uuid>,
|
||||
billing_state_author -> Nullable<Uuid>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,6 +153,7 @@ table! {
|
|||
position_instance_id -> Uuid,
|
||||
real_start_time -> Nullable<Timestamptz>,
|
||||
real_end_time -> Nullable<Timestamptz>,
|
||||
billable -> Nullable<Bool>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -576,12 +580,12 @@ joinable!(addresses_entities -> addresses (address_id));
|
|||
joinable!(addresses_entities -> entities (entitiy_id));
|
||||
joinable!(appointments -> appointment_types (type_id));
|
||||
joinable!(appointments -> entities (entity_id));
|
||||
joinable!(billing_states -> entities (entity_id));
|
||||
joinable!(buildings -> entities (entity_id));
|
||||
joinable!(communication_targets -> communication_types (com_type));
|
||||
joinable!(communication_targets -> entities (entity_id));
|
||||
joinable!(cost_centres_members -> cost_centres (cost_centre_shortid));
|
||||
joinable!(cost_centres_members -> members (member_entity_id));
|
||||
joinable!(eu_instances -> billing_states (billing_state_id));
|
||||
joinable!(eu_instances -> personnel_billing_rates (billing_rate_id));
|
||||
joinable!(eu_positions -> entities (entity_id));
|
||||
joinable!(eu_templates -> entities (entity_id));
|
||||
|
|
Loading…
Reference in New Issue