Compare commits

...

18 Commits

Author SHA1 Message Date
anghenfil 9bb55d7097 Merge pull request 'Finished billing module' (#24) from billing_module into develop
Reviewed-on: #24
2022-03-08 11:50:19 +00:00
anghenfil 4bebe1b1a1 Merge branch 'develop' into billing_module 2022-03-08 11:50:10 +00:00
Keanu D?lle f72dab60a9 FEA: finished approve method in event_billig. Finished event billing module 2022-03-08 12:48:32 +01:00
Keanu D?lle 709dd54052 FIX: fixed #4 (#4) 2022-03-08 12:47:32 +01:00
Keanu D?lle 4e5ee0d146 FEA: migrated mail setup to use lettre
Now we don't depend on cli mail client mail(x), but use smtp.
2022-03-08 12:46:19 +01:00
Keanu D?lle bbf971b345 added new billing settings 2022-03-08 12:43:54 +01:00
Keanu D?lle 136781c1f8 FEA: Approve billing stages 2022-03-08 00:51:33 +01:00
Keanu D?lle 779e3fa74b FIX: dependency update errors 2022-03-06 22:37:27 +01:00
Keanu D?lle 6132975f7a updated dependencies 2022-03-06 22:20:30 +01:00
Keanu D?lle fcd8c15012 FEA: added links to progressbar steps 2022-03-06 19:31:36 +01:00
Keanu D?lle 3ba1f5f4b7 FEA: add missing real_start_time/real_end_time from planned time / instance times 2022-03-06 19:31:05 +01:00
Keanu D?lle 930b8141a0 FEA: implemented the edit times view 2022-03-06 07:39:57 +01:00
Keanu D?lle f22dcf2a3c improved documentation for Patchs 2022-03-06 07:36:49 +01:00
Keanu D?lle b597925b3c fix: use proper deprecated notice 2022-03-06 07:36:24 +01:00
Keanu D?lle b23abb9a28 FEA: new search2 as mini_searchbar replacement 2022-03-06 07:35:00 +01:00
Keanu D?lle 22734025fb FIX: protected position_id foreign key in position_instances against deletion
Changed on delete cascade to on delete restrict
2022-03-06 07:32:58 +01:00
Keanu D?lle 33fe34c60f improved config description 2022-03-06 07:31:53 +01:00
Keanu D?lle 2a6bdead8b FEA: added billing module (WIP) 2022-02-24 02:28:53 +01:00
91 changed files with 4743 additions and 648 deletions

491
Cargo.lock generated
View File

@ -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]]

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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';

View File

@ -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);

View File

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
alter table billing_states
drop column "order";

View File

@ -0,0 +1,3 @@
-- Your SQL goes here
alter table billing_states
add "order" int;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
alter table eu_position_instances
drop column billable;

View File

@ -0,0 +1,3 @@
-- Your SQL goes here
alter table eu_position_instances
add billable bool;

View File

@ -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;

View File

@ -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;

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -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">

View File

@ -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>

View File

@ -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}}

View File

@ -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>

View File

@ -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;
}

View File

@ -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 = {};

View File

@ -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
}

View File

@ -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
}
}());

View File

@ -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;
}
}

View File

@ -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")

186
resources/js/search2.js Normal file
View File

@ -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
}
}());

View File

@ -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}}

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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">

View File

@ -1 +1 @@
v0.2-80-gf8eca4a
v0.2-96-g136781c

View File

@ -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,

View File

@ -0,0 +1,3 @@
pub mod states;
pub mod personnel_billing_rates;
pub mod personnel_billing;

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)?;

View File

@ -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;

View File

@ -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())
}

View File

@ -1,2 +1,3 @@
pub mod queue;
pub mod worker;
pub mod worker;
pub mod mailer;

View File

@ -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(&current) { //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
}
}
}
}*/
}

View File

@ -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(())
}
}

View File

@ -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),

View File

@ -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()
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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]

View File

@ -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"))

View File

@ -0,0 +1 @@
pub mod read;

View File

@ -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)),
}
}

View File

@ -0,0 +1 @@
pub mod read;

View File

@ -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)),
}
}

View File

@ -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()))
}
}

View File

@ -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){

View File

@ -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))
}
}

View File

@ -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))
}

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -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)),
}
}

View File

@ -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(_) => {}

View File

@ -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))
}
}

View File

@ -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;

View File

@ -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())
}
}

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -0,0 +1,4 @@
pub mod read;
pub mod delete;
pub mod create;
pub mod calculation;

View File

@ -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))
}
}

View File

@ -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,
}))
}

View File

@ -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,
}))
}

View File

@ -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,
}))
}

View File

@ -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,
}))
}

View File

@ -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,
}))
}

View File

@ -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)\""))
}
}

View File

@ -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;

View File

@ -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(),
}],
};

View File

@ -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(())
}

View File

@ -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));