diff --git a/.gitattributes b/.gitattributes index d528e69e3..3b75a78dd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ # https://warlord0blog.wordpress.com/2019/09/04/vscode-crlf-vs-lf-battle/ - text=lf +text=lf *.css linguist-vendored eol=lf *.scss linguist-vendored eol=lf *.js linguist-vendored eol=lf diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c6fee8ab4..4816bd1e2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -17,9 +17,10 @@ To test this backend PR you need to checkout the #XXX frontend PR. ## How to test: 1. check into current branch 2. do `npm install` and `...` to run this PR locally -3. log as admin user -4. go to dashboard→ Tasks→ task→… -5. verify function “A” (feel free to include screenshot here) +3. Clear site data/cache +4. log as admin user +5. go to dashboard→ Tasks→ task→… +6. verify function “A” (feel free to include screenshot here) ## Screenshots or videos of changes: diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..518633e16 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +lts/fermium diff --git a/package-lock.json b/package-lock.json index 3fcc98986..ecce49ec7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1568,7 +1568,7 @@ "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, "@types/mime": { @@ -1729,7 +1729,7 @@ "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "array-includes": { "version": "3.1.6", @@ -2768,7 +2768,7 @@ "bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, "bignumber.js": { "version": "9.0.2", @@ -2863,7 +2863,7 @@ "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, "buffer-from": { "version": "1.1.2", @@ -2995,7 +2995,7 @@ "clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" }, "clone-deep": { "version": "4.0.1", @@ -3039,12 +3039,12 @@ "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "concat-stream": { "version": "1.6.2", @@ -3100,7 +3100,7 @@ "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "core-js": { "version": "3.21.1", @@ -3253,7 +3253,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { "version": "1.4.81", @@ -3269,7 +3269,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "es-abstract": { "version": "1.19.1", @@ -3350,7 +3350,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "escape-string-regexp": { "version": "1.0.5", @@ -4264,7 +4264,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "event-target-shim": { "version": "5.0.1", @@ -4409,6 +4409,22 @@ } } }, + "express-validator": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", + "integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==", + "requires": { + "lodash": "^4.17.21", + "validator": "^13.9.0" + }, + "dependencies": { + "validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -4429,7 +4445,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "fast-text-encoding": { @@ -4559,7 +4575,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, "fs-readdir-recursive": { "version": "1.1.0", @@ -4569,7 +4585,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "fsevents": { "version": "2.3.2", @@ -4952,7 +4968,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, "indent-string": { @@ -4964,7 +4980,7 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -5069,7 +5085,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, "is-glob": { "version": "4.0.3", @@ -5171,13 +5187,13 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" }, "js-tokens": { "version": "4.0.0", @@ -5215,7 +5231,7 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "json5": { @@ -5536,7 +5552,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, "lodash.merge": { "version": "4.6.2", @@ -5713,7 +5729,7 @@ "lru_map": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", - "integrity": "sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=" + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==" }, "make-dir": { "version": "2.1.0", @@ -5727,7 +5743,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "memory-pager": { "version": "1.5.0", @@ -5738,7 +5754,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "merge-stream": { "version": "2.0.0", @@ -5749,7 +5765,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { "version": "4.0.5", @@ -5929,7 +5945,7 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "negotiator": { @@ -6069,7 +6085,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { "version": "1.12.0", @@ -6788,7 +6804,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "requires": { "wrappy": "1" } @@ -6818,7 +6834,7 @@ "os-shim": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=", + "integrity": "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==", "dev": true }, "p-limit": { @@ -6863,7 +6879,7 @@ "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==" }, "parseurl": { "version": "1.3.3", @@ -6878,7 +6894,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, "path-key": { "version": "3.1.1", @@ -6894,7 +6910,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "picocolors": { "version": "1.0.0", @@ -6941,7 +6957,7 @@ "pre-commit": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz", - "integrity": "sha1-287g7p3nI15X95xW186UZBpp7sY=", + "integrity": "sha512-qokTiqxD6GjODy5ETAIgzsRgnBWWQHQH2ghy86PU7mIn/wuWeTwF3otyNQZxWBwVn8XNr8Tdzj/QfUXpH+gRZA==", "dev": true, "requires": { "cross-spawn": "^5.0.1", @@ -6952,7 +6968,7 @@ "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", "dev": true, "requires": { "lru-cache": "^4.0.1", @@ -6963,7 +6979,7 @@ "which": { "version": "1.2.14", "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "integrity": "sha512-16uPglFkRPzgiUXYMi1Jf8Z5EzN1iB4V0ZtMXcHZnwsBtQhhHeCqoWw7tsUY42hJGNDWtUsVLTjakIa5BgAxCw==", "dev": true, "requires": { "isexe": "^2.0.0" @@ -7005,7 +7021,7 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", "dev": true }, "pstree.remy": { @@ -7483,7 +7499,7 @@ "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", "optional": true, "requires": { "memory-pager": "^1.0.2" @@ -7492,7 +7508,7 @@ "spawn-sync": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", + "integrity": "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==", "dev": true, "requires": { "concat-stream": "^1.4.7", @@ -7887,7 +7903,7 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true }, "strip-final-newline": { @@ -7918,19 +7934,19 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" }, "to-regex-range": { "version": "5.0.1", @@ -7957,7 +7973,7 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "tsconfig-paths": { "version": "3.14.2", @@ -8075,7 +8091,7 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "dev": true }, "unbox-primitive": { @@ -8122,7 +8138,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, "uri-js": { "version": "4.4.1", @@ -8136,17 +8152,17 @@ "url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE=" + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, "uuid": { "version": "3.4.0", @@ -8169,17 +8185,17 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -8297,7 +8313,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { "version": "8.8.1", diff --git a/package.json b/package.json index 1c6b8a5d4..e7fbd6307 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "cron": "^1.8.2", "dotenv": "^5.0.1", "express": "^4.17.1", + "express-validator": "^7.0.1", "googleapis": "^100.0.0", "jsonwebtoken": "^9.0.0", "lodash": "^4.17.21", diff --git a/src/controllers/REAL_TIME_timerController.js b/src/controllers/REAL_TIME_timerController.js deleted file mode 100644 index 699dcddef..000000000 --- a/src/controllers/REAL_TIME_timerController.js +++ /dev/null @@ -1,155 +0,0 @@ - -const logger = require('../startup/logger'); -const OldTimer = require('../models/oldTimer'); - -const timerController = function (Timer) { - const getTimerFromDatabase = async ({ userId }) => { - try { - const timerObject = await Timer.findOne({ userId }).exec(); - if (!timerObject) { - const newRecord = { - userId, - totalSeconds: 0, - isRunning: false, - isApplicationPaused: false, - isUserPaused: false, - }; - const newTimer = await Timer.create(newRecord); - return newTimer; - } - return timerObject; - } catch (e) { - logger.logException(e); - throw new Error('Issue trying to retrieve timer data from MongoDB'); - } - }; - - const setTimerToDatabase = async ({ - userId, - timerObject: { - totalSeconds, - isRunning, - isUserPaused, - isApplicationPaused, - } = {}, - } = {}) => { - try { - const update = { - $set: { - totalSeconds, - isRunning, - isUserPaused, - isApplicationPaused, - }, - }; - - const options = { - upsert: true, - new: true, - setDefaultsOnInsert: true, - rawResult: true, - }; - - return await Timer.findOneAndUpdate({ userId }, update, options).exec(); - } catch (e) { - logger.logException(e); - throw new Error('Issue trying to set timer data from MongoDB'); - } - }; - - const putTimer = function (req, res) { - const { userId } = req.params; - - const query = { userId }; - const update = { - $set: { - pausedAt: req.body.pausedAt, - isWorking: req.body.isWorking, - started: req.body.isWorking ? Date.now() : null, - lastAccess: Date.now(), - }, - }; - const options = { - upsert: true, new: true, setDefaultsOnInsert: true, rawResult: true, - }; - - OldTimer.findOneAndUpdate(query, update, options, (error, rawResult) => { - if (error) { - return res.status(500).send({ error }); - } - - if (rawResult === null || rawResult.value === undefined || rawResult.value === null - || rawResult.lastErrorObject === null || rawResult.lastErrorObject === undefined - || rawResult.value.length === 0) { - return res.status(500).send('Update/Upsert timer date failed'); - } - - if (rawResult.lastErrorObject.updatedExisting === true) { - return res.status(200).send({ message: 'updated timer data' }); - } - if (rawResult.lastErrorObject.updatedExisting === false - && rawResult.lastErrorObject.upserted !== undefined && rawResult.lastErrorObject.upserted !== null) { - return res.status(201).send({ _id: rawResult.lastErrorObject.upserted }); - } - return res.status(500).send('Update/Upsert timer date failed'); - }); - }; - - const timePassed = (timer) => { - if (!timer.started) { return 0; } - const now = timer.timedOut ? timer.lastAccess : Date.now(); - return Math.floor((now - timer.started) / 1000); - }; - - const adjust = (timer, cb) => { - const oneMin = 60 * 1000; - const fiveMin = 5 * oneMin; - const timeSinceLastAccess = timer.lastAccess ? (Date.now() - timer.lastAccess) : 0; - const setLastAccess = !timer.lastAccess || (timeSinceLastAccess > oneMin); - - timer.timedOut = timer.isWorking && (timeSinceLastAccess > fiveMin); - timer.seconds = timer.pausedAt + timePassed(timer); - - if (timer.timedOut) { - return OldTimer.findOneAndUpdate({ userId: timer.userId }, { - isWorking: false, - pauseAt: timer.seconds, - started: null, - lastAccess: Date.now(), - }).then(() => cb(timer)); - } - if (setLastAccess) { - return OldTimer.findOneAndUpdate({ userId: timer.userId }, { lastAccess: Date.now() }).then(() => cb(timer)); - } - - return cb(timer); - }; - - const getTimer = function (req, res) { - const { userId } = req.params; - - OldTimer.findOne({ userId }).lean().exec((error, record) => { - if (error) { - return res.status(500).send(error); - } - if (record === null) { - if (req.body.requestor.requestorId === userId) { - const newRecord = { - userId, - pausedAt: 0, - isWorking: false, - }; - return OldTimer.create(newRecord).then(result => res.status(200).send(result)).catch(() => res.status(400).send('Timer record not found for the given user ID')); - } - return res.status(400).send('Timer record not found for the given user ID'); - } - return adjust(record, (timer) => { res.status(200).send(timer); }); - }); - }; - - return { - putTimer, getTimer, getTimerFromDatabase, setTimerToDatabase, - }; -}; - -module.exports = timerController; diff --git a/src/controllers/badgeController.js b/src/controllers/badgeController.js index 62bad6399..5dd2113a6 100644 --- a/src/controllers/badgeController.js +++ b/src/controllers/badgeController.js @@ -1,11 +1,14 @@ +const moment = require('moment-timezone'); const mongoose = require('mongoose'); const UserProfile = require('../models/userProfile'); const { hasPermission } = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); +const cache = require('../utilities/nodeCache')(); +const logger = require('../startup/logger'); const badgeController = function (Badge) { const getAllBadges = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'seeBadges')) { + if (!(await hasPermission(req.body.requestor, 'seeBadges'))) { res.status(403).send('You are not authorized to view all badge data.'); return; } @@ -13,10 +16,11 @@ const badgeController = function (Badge) { Badge.find( {}, 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', - ).populate({ - path: 'project', - select: '_id projectName', - }) + ) + .populate({ + path: 'project', + select: '_id projectName', + }) .sort({ ranking: 1, badgeName: 1, @@ -25,8 +29,32 @@ const badgeController = function (Badge) { .catch(error => res.status(404).send(error)); }; + /** + * Updated Date: 12/06/2023 + * Updated By: Shengwei + * Function added: + * - Added data validation for earned date and badge count mismatch. + * - Added fillEarnedDateToMatchCount function to resolve earned date and badge count mismatch. + * - Refactored data validation for duplicate badge id. + * - Added data validation for badge count should greater than 0. + * - Added formatDate function to format date to MMM-DD-YY. + */ + + const formatDate = () => { + const currentDate = new Date(Date.now()); + return moment(currentDate).tz('America/Los_Angeles').format('MMM-DD-YY'); + }; + + const fillEarnedDateToMatchCount = (earnedDate, count) => { + const result = [...earnedDate]; + while (result.length < count) { + result.push(formatDate()); + } + return result; + }; + const assignBadges = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'assignBadges')) { + if (!(await hasPermission(req.body.requestor, 'assignBadges'))) { res.status(403).send('You are not authorized to assign badges.'); return; } @@ -38,61 +66,100 @@ const badgeController = function (Badge) { res.status(400).send('Can not find the user to be assigned.'); return; } - const grouped = req.body.badgeCollection.reduce((groupd, item) => { - const propertyValue = item.badge; - groupd[propertyValue] = (groupd[propertyValue] || 0) + 1; - return groupd; - }, {}); - const result = Object.keys(grouped).every(bdge => grouped[bdge] <= 1); - if (result) { - record.badgeCollection = req.body.badgeCollection; - - record.save() - .then(results => res.status(201).send(results._id)) - .catch(errors => res.status(500).send(errors)); - } else { - res.status(500).send('Duplicate badges sent in.'); + const badgeCounts = {}; + // This line is using the forEach function to group badges in the badgeCollection + // array in the request body. + // Validation: No duplicate badge id; + try { + req.body.badgeCollection.forEach((element) => { + if (badgeCounts[element.badge]) { + throw new Error('Duplicate badges sent in.'); + // res.status(500).send('Duplicate badges sent in.'); + // return; + } + badgeCounts[element.badge] = element.count; + // Validation: count should be greater than 0 + if (element.count < 1) { + throw new Error('Badge count should be greater than 0.'); + } + if (element.count !== element.earnedDate.length) { + element.earnedDate = fillEarnedDateToMatchCount( + element.earnedDate, + element.count, + ); + element.lastModified = Date.now(); + logger.logInfo( + `Badge count and earned dates mismatched found. ${Date.now()} was generated for user ${userToBeAssigned}. Badge record ID ${ + element._id + }; Badge Type ID ${element.badge}`, + ); + } + }); + } catch (err) { + res.status(500).send(`Internal Error: Badge Collection. ${ err.message}`); + return; + } + record.badgeCollection = req.body.badgeCollection; + + if (cache.hasCache(`user-${userToBeAssigned}`)) { + cache.removeCache(`user-${userToBeAssigned}`); } + // Save Updated User Profile + record + .save() + .then(results => res.status(201).send(results._id)) + .catch((err) => { + logger.logException(err); + res.status(500).send('Internal Error: Unable to save the record.'); + }); }); }; const postBadge = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'createBadges')) { - res.status(403).send({ error: 'You are not authorized to create new badges.' }); + if (!(await hasPermission(req.body.requestor, 'createBadges'))) { + res + .status(403) + .send({ error: 'You are not authorized to create new badges.' }); return; } - Badge.find({ badgeName: { $regex: escapeRegex(req.body.badgeName), $options: 'i' } }) - .then((result) => { - if (result.length > 0) { - res.status(400).send({ error: `Another badge with name ${result[0].badgeName} already exists. Sorry, but badge names should be like snowflakes, no two should be the same. Please choose a different name for this badge so it can be proudly unique.` }); - return; - } - const badge = new Badge(); - - badge.badgeName = req.body.badgeName; - badge.category = req.body.category; - badge.type = req.body.type; - badge.multiple = req.body.multiple; - badge.totalHrs = req.body.totalHrs; - badge.weeks = req.body.weeks; - badge.months = req.body.months; - badge.people = req.body.people; - badge.project = req.body.project; - badge.imageUrl = req.body.imageUrl; - badge.ranking = req.body.ranking; - badge.description = req.body.description; - badge.showReport = req.body.showReport; - - badge.save() - .then(results => res.status(201).send(results)) - .catch(errors => res.status(500).send(errors)); - }); + Badge.find({ + badgeName: { $regex: escapeRegex(req.body.badgeName), $options: 'i' }, + }).then((result) => { + if (result.length > 0) { + res.status(400).send({ + error: `Another badge with name ${result[0].badgeName} already exists. Sorry, but badge names should be like snowflakes, no two should be the same. Please choose a different name for this badge so it can be proudly unique.`, + }); + return; + } + const badge = new Badge(); + + badge.badgeName = req.body.badgeName; + badge.category = req.body.category; + badge.type = req.body.type; + badge.multiple = req.body.multiple; + badge.totalHrs = req.body.totalHrs; + badge.weeks = req.body.weeks; + badge.months = req.body.months; + badge.people = req.body.people; + badge.project = req.body.project; + badge.imageUrl = req.body.imageUrl; + badge.ranking = req.body.ranking; + badge.description = req.body.description; + badge.showReport = req.body.showReport; + + badge + .save() + .then(results => res.status(201).send(results)) + .catch(errors => res.status(500).send(errors)); + }); }; const deleteBadge = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'deleteBadges')) { - res.status(403).send({ error: 'You are not authorized to delete badges.' }); + if (!(await hasPermission(req.body.requestor, 'deleteBadges'))) { + res + .status(403) + .send({ error: 'You are not authorized to delete badges.' }); return; } const { badgeId } = req.params; @@ -101,19 +168,31 @@ const badgeController = function (Badge) { res.status(400).send({ error: 'No valid records found' }); return; } - const removeBadgeFromProfile = UserProfile.updateMany({}, { $pull: { badgeCollection: { badge: record._id } } }).exec(); + const removeBadgeFromProfile = UserProfile.updateMany( + {}, + { $pull: { badgeCollection: { badge: record._id } } }, + ).exec(); const deleteRecord = record.remove(); Promise.all([removeBadgeFromProfile, deleteRecord]) - .then(res.status(200).send({ message: 'Badge successfully deleted and user profiles updated' })) - .catch((errors) => { res.status(500).send(errors); }); - }) - .catch((error) => { res.status(500).send(error); }); + .then( + res.status(200).send({ + message: 'Badge successfully deleted and user profiles updated', + }), + ) + .catch((errors) => { + res.status(500).send(errors); + }); + }).catch((error) => { + res.status(500).send(error); + }); }; const putBadge = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'updateBadges')) { - res.status(403).send({ error: 'You are not authorized to update badges.' }); + if (!(await hasPermission(req.body.requestor, 'updateBadges'))) { + res + .status(403) + .send({ error: 'You are not authorized to update badges.' }); return; } const { badgeId } = req.params; @@ -126,7 +205,6 @@ const badgeController = function (Badge) { // store onto Azure and return url } - const data = { badgeName: req.body.name || req.body.badgeName, description: req.body.description, diff --git a/src/controllers/bmdashboard/bmConsumableController.js b/src/controllers/bmdashboard/bmConsumableController.js new file mode 100644 index 000000000..23ea6e5cd --- /dev/null +++ b/src/controllers/bmdashboard/bmConsumableController.js @@ -0,0 +1,46 @@ +const bmConsumableController = function (BuildingConsumable) { + const fetchBMConsumables = async (req, res) => { + try { + BuildingConsumable + .find() + .populate([ + { + path: 'project', + select: '_id name', + }, + { + path: 'itemType', + select: '_id name unit', + }, + { + path: 'updateRecord', + populate: { + path: 'createdBy', + select: '_id firstName lastName', + }, + }, + { + path: 'purchaseRecord', + populate: { + path: 'requestedBy', + select: '_id firstName lastName', + }, + }, + ]) + .exec() + .then(result => { + res.status(200).send(result); + }) + .catch(error => res.status(500).send(error)); + } catch (err) { + res.json(err); + } + }; + + return { + fetchBMConsumables, + }; + }; + +module.exports = bmConsumableController; + \ No newline at end of file diff --git a/src/controllers/bmdashboard/bmInventoryTypeController.js b/src/controllers/bmdashboard/bmInventoryTypeController.js new file mode 100644 index 000000000..76029a42b --- /dev/null +++ b/src/controllers/bmdashboard/bmInventoryTypeController.js @@ -0,0 +1,100 @@ +function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolType, EquipType) { + async function fetchMaterialTypes(req, res) { + try { + MatType + .find() + .exec() + .then(result => res.status(200).send(result)) + .catch(error => res.status(500).send(error)); + } catch (err) { + res.json(err); + } + } + + async function addEquipmentType(req, res) { + const { + name, + desc: description, + fuel: fuelType, + requestor: { requestorId }, + } = req.body; + try { + EquipType + .find({ name }) + .then((result) => { + if (result.length) { + res.status(409).send(); + } else { + const newDoc = { + category: 'Equipment', + name, + description, + fuelType, + createdBy: requestorId, + }; + EquipType + .create(newDoc) + .then(() => res.status(201).send()) + .catch((error) => { + if (error._message.includes('validation failed')) { + res.status(400).send(error); + } else { + res.status(500).send(error); + } + }); + } + }) + .catch(error => res.status(500).send(error)); + } catch (error) { + res.status(500).send(error); + } + } + const fetchSingleInventoryType = async (req, res) => { + const { invtypeId } = req.params; + try { + const result = await InvType.findById(invtypeId).exec(); + res.status(200).send(result); + } catch (error) { + res.status(500).send(error); + } + }; + + const updateNameAndUnit = async (req, res) => { + try { + const { invtypeId } = req.params; + const { name, unit } = req.body; + + const updateData = {}; + + if (name) { + updateData.name = name; + } + + if (unit) { + updateData.unit = unit; + } + + const updatedInvType = await InvType.findByIdAndUpdate( + invtypeId, + updateData, + { new: true, runValidators: true }, + ); + + if (!updatedInvType) { + return res.status(404).json({ error: 'invType Material not found check Id' }); + } + + res.status(200).json(updatedInvType); + } catch (error) { + res.status(500).send(error); + } + }; + return { + fetchMaterialTypes, + addEquipmentType, + fetchSingleInventoryType, + updateNameAndUnit, + }; +} + +module.exports = bmInventoryTypeController; diff --git a/src/controllers/bmdashboard/bmMaterialsController.js b/src/controllers/bmdashboard/bmMaterialsController.js index a31ed460e..207e2428a 100644 --- a/src/controllers/bmdashboard/bmMaterialsController.js +++ b/src/controllers/bmdashboard/bmMaterialsController.js @@ -1,48 +1,210 @@ -const mongoose = require('mongoose') +const mongoose = require('mongoose'); -const bmMaterialsController = function (ItemMaterial) { +const bmMaterialsController = function (BuildingMaterial) { const bmMaterialsList = async function _matsList(req, res) { try { - ItemMaterial.find() + BuildingMaterial.find() .populate([ { path: 'project', - select: '_id projectName' + select: '_id name', }, { - path: 'inventoryItemType', - select: '_id name uom totalStock totalAvailable' - }, - { - path: 'usageRecord', - populate: { - path: 'createdBy', - select: '_id firstName lastName' - } + path: 'itemType', + select: '_id name unit', }, { path: 'updateRecord', populate: { path: 'createdBy', - select: '_id firstName lastName' - } + select: '_id firstName lastName', + }, }, { path: 'purchaseRecord', populate: { - path: 'createdBy', - select: '_id firstName lastName' - } - } + path: 'requestedBy', + select: '_id firstName lastName', + }, + }, ]) .exec() .then(results => res.status(200).send(results)) - .catch(error => res.status(500).send(error)) + .catch(error => res.status(500).send(error)); } catch (err) { res.json(err); } }; - return { bmMaterialsList }; + + const bmPurchaseMaterials = async function (req, res) { + const { + projectId, + matTypeId, + quantity, + priority, + brand: brandPref, + requestor: { requestorId }, + } = req.body; + try { + // check if requestor has permission to make purchase request + //! Note: this code is disabled until permissions are added + // TODO: uncomment this code to execute auth check + // const { buildingManager: bmId } = await buildingProject.findById(projectId, 'buildingManager').exec(); + // if (bmId !== requestorId) { + // res.status(403).send({ message: 'You are not authorized to edit this record.' }); + // return; + // } + + // check if the material is already being used in the project + // if no, add a new document to the collection + // if yes, update the existing document + const newPurchaseRecord = { + quantity, + priority, + brandPref, + requestedBy: requestorId, + }; + const doc = await BuildingMaterial.findOne({ project: projectId, itemType: matTypeId }); + if (!doc) { + const newDoc = { + itemType: matTypeId, + project: projectId, + purchaseRecord: [newPurchaseRecord], + }; + BuildingMaterial + .create(newDoc) + .then(() => res.status(201).send()) + .catch(error => res.status(500).send(error)); + return; + } + BuildingMaterial + .findOneAndUpdate( + { _id: mongoose.Types.ObjectId(doc._id) }, + { $push: { purchaseRecord: newPurchaseRecord } }, + ) + .exec() + .then(() => res.status(201).send()) + .catch(error => res.status(500).send(error)); + } catch (error) { + res.status(500).send(error); + } + }; + + const bmPostMaterialUpdateRecord = function (req, res) { + const payload = req.body; + let quantityUsed = +req.body.quantityUsed; + let quantityWasted = +req.body.quantityWasted; + const { material } = req.body; + if (payload.QtyUsedLogUnit == 'percent' && quantityWasted >= 0) { + quantityUsed = +((+quantityUsed / 100) * material.stockAvailable).toFixed(4); + } + if (payload.QtyWastedLogUnit == 'percent' && quantityUsed >= 0) { + quantityWasted = +((+quantityWasted / 100) * material.stockAvailable).toFixed(4); + } + + if (quantityUsed > material.stockAvailable || quantityWasted > material.stockAvailable || (quantityUsed + quantityWasted) > material.stockAvailable) { + res.status(500).send('Please check the used and wasted stock values. Either individual values or their sum exceeds the total stock available.'); + } else { + let newStockUsed = +material.stockUsed + parseFloat(quantityUsed); + let newStockWasted = +material.stockWasted + parseFloat(quantityWasted); + let newAvailable = +material.stockAvailable - parseFloat(quantityUsed) - parseFloat(quantityWasted); + newStockUsed = parseFloat(newStockUsed.toFixed(4)); + newStockWasted = parseFloat(newStockWasted.toFixed(4)); + newAvailable = parseFloat(newAvailable.toFixed(4)); + BuildingMaterial.updateOne( + { _id: req.body.material._id }, + + { + $set: { + stockUsed: newStockUsed, + stockWasted: newStockWasted, + stockAvailable: newAvailable, + }, + $push: { + updateRecord: { + date: req.body.date, + createdBy: req.body.requestor.requestorId, + quantityUsed, + quantityWasted, + }, + }, + }, + + ) + .then((results) => { res.status(200).send(results); }) + .catch(error => res.status(500).send({ message: error })); + } + }; + + const bmPostMaterialUpdateBulk = function (req, res) { + const materialUpdates = req.body.upadateMaterials; + let errorFlag = false; + const updateRecordsToBeAdded = []; + for (let i = 0; i < materialUpdates.length; i++) { + const payload = materialUpdates[i]; + let quantityUsed = +payload.quantityUsed; + let quantityWasted = +payload.quantityWasted; + const { material } = payload; + if (payload.QtyUsedLogUnit == 'percent' && quantityWasted >= 0) { + quantityUsed = +((+quantityUsed / 100) * material.stockAvailable).toFixed(4); + } + if (payload.QtyWastedLogUnit == 'percent' && quantityUsed >= 0) { + quantityWasted = +((+quantityWasted / 100) * material.stockAvailable).toFixed(4); + } + + let newStockUsed = +material.stockUsed + parseFloat(quantityUsed); + let newStockWasted = +material.stockWasted + parseFloat(quantityWasted); + let newAvailable = +material.stockAvailable - parseFloat(quantityUsed) - parseFloat(quantityWasted); + newStockUsed = parseFloat(newStockUsed.toFixed(4)); + newStockWasted = parseFloat(newStockWasted.toFixed(4)); + newAvailable = parseFloat(newAvailable.toFixed(4)); + if (newAvailable < 0) { + errorFlag = true; + break; + } + updateRecordsToBeAdded.push({ + updateId: material._id, + set: { + stockUsed: newStockUsed, + stockWasted: newStockWasted, + stockAvailable: newAvailable, + }, + updateValue: { + createdBy: req.body.requestor.requestorId, + quantityUsed, + quantityWasted, + date: req.body.date, + }, +}); + } + + try { + if (errorFlag) { + res.status(500).send('Stock quantities submitted seems to be invalid'); + return; + } + const updatePromises = updateRecordsToBeAdded.map(updateItem => BuildingMaterial.updateOne( + { _id: updateItem.updateId }, + { + $set: updateItem.set, + $push: { updateRecord: updateItem.updateValue }, + }, + ).exec()); + Promise.all(updatePromises) + .then((results) => { + res.status(200).send({ result: `Successfully posted log for ${results.length} Material records.` }); + }) + .catch(error => res.status(500).send(error)); + } catch (err) { + res.json(err); + } + }; + return { + bmMaterialsList, + bmPostMaterialUpdateRecord, + bmPostMaterialUpdateBulk, + bmPurchaseMaterials, +}; }; -module.exports = bmMaterialsController; \ No newline at end of file +module.exports = bmMaterialsController; diff --git a/src/controllers/bmdashboard/bmProjectController.js b/src/controllers/bmdashboard/bmProjectController.js new file mode 100644 index 000000000..a4f6712e0 --- /dev/null +++ b/src/controllers/bmdashboard/bmProjectController.js @@ -0,0 +1,128 @@ +/* eslint-disable prefer-destructuring */ +// TODO: uncomment when executing auth checks +// const jwt = require('jsonwebtoken'); +// const config = require('../../config'); + +const bmMProjectController = function (BuildingProject) { + // TODO: uncomment when executing auth checks + // const { JWT_SECRET } = config; + + const fetchAllProjects = async (req, res) => { + //! Note: for easier testing this route currently returns all projects from the db + // TODO: uncomment the lines below to return only projects where field buildingManager === userid + // const token = req.headers.authorization; + // const { userid } = jwt.verify(token, JWT_SECRET); + try { + BuildingProject.aggregate([ + { + $match: { isActive: true }, + }, + { + $lookup: { + from: 'userProfiles', + let: { id: '$buildingManager' }, + pipeline: [ + { $match: { $expr: { $eq: ['$_id', '$$id'] } } }, + { $project: { firstName: 1, lastName: 1, email: 1 } }, + ], + as: 'buildingManager', + }, + }, + { $unwind: '$buildingManager' }, + { + $lookup: { + from: 'buildingInventoryItems', + let: { id: '$_id' }, + pipeline: [ + { $match: { $expr: { $eq: ['$project', '$$id'] } } }, + { $match: { __t: 'material_item' } }, + { $project: { updateRecord: 0, project: 0 } }, + { + $lookup: { + from: 'buildingInventoryTypes', + localField: 'itemType', + foreignField: '_id', + as: 'itemType', + }, + }, + { + $unwind: '$itemType', + }, + ], + as: 'materials', + }, + }, + { + $project: { + name: 1, + isActive: 1, + template: 1, + location: 1, + dateCreated: 1, + buildingManager: 1, + teams: 1, + members: 1, + materials: 1, + hoursWorked: { $sum: '$members.hours' }, + // cost values can be calculated once a process for purchasing inventory is created + totalMaterialsCost: { $sum: 1500 }, + totalEquipmentCost: { $sum: 3000 }, + }, + }, + ]) + .then((results) => { + results.forEach((proj) => { + proj.mostMaterialWaste = proj.materials.sort((a, b) => b.stockWasted - a.stockWasted)[0]; + proj.leastMaterialAvailable = proj.materials.sort((a, b) => a.stockAvailable - b.stockAvailable)[0]; + proj.mostMaterialBought = proj.materials.sort((a, b) => b.stockBought - a.stockBought)[0]; + }); + res.status(200).send(results); + }) + .catch(error => res.status(500).send(error)); + } catch (err) { + res.status(500).send(err); + } + }; + + // fetches single project by project id + const fetchSingleProject = async (req, res) => { + //! Note: for easier testing this route currently returns the project without an auth check + // TODO: uncomment the lines below to check the user's ability to view the current project + // const token = req.headers.authorization; + // const { userid } = jwt.verify(token, JWT_SECRET); + const { projectId } = req.params; + try { + BuildingProject + .findById(projectId) + .populate([ + { + path: 'buildingManager', + select: '_id firstName lastName email', + }, + { + path: 'team', + select: '_id firstName lastName email', + }, + ]) + .exec() + .then(project => res.status(200).send(project)) + // TODO: uncomment this block to execute the auth check + // authenticate request by comparing userId param with buildingManager id field + // Note: _id has type object and must be converted to string + // .then((project) => { + // if (userid !== project.buildingManager._id.toString()) { + // return res.status(403).send({ + // message: 'You are not authorized to view this record.', + // }); + // } + // return res.status(200).send(project); + // }) + .catch(error => res.status(500).send(error)); + } catch (err) { + res.json(err); + } + }; + return { fetchAllProjects, fetchSingleProject }; +}; + +module.exports = bmMProjectController; diff --git a/src/controllers/bmdashboard/bmToolController.js b/src/controllers/bmdashboard/bmToolController.js new file mode 100644 index 000000000..0feec256c --- /dev/null +++ b/src/controllers/bmdashboard/bmToolController.js @@ -0,0 +1,52 @@ +const bmToolController = (BuildingTool) => { + const fetchSingleTool = async (req, res) => { + const { toolId } = req.params; + try { + BuildingTool + .findById(toolId) + .populate([ + { + path: 'itemType', + select: '_id name description unit imageUrl category', + }, + { + path: 'userResponsible', + select: '_id firstName lastName', + }, + { + path: 'purchaseRecord', + populate: { + path: 'requestedBy', + select: '_id firstName lastName', + }, + }, + { + path: 'updateRecord', + populate: { + path: 'createdBy', + select: '_id firstName lastName', + }, + }, + { + path: 'logRecord', + populate: [{ + path: 'createdBy', + select: '_id firstName lastName', + }, + { + path: 'responsibleUser', + select: '_id firstName lastName', + }], + }, + ]) + .exec() + .then(tool => res.status(200).send(tool)) + .catch(error => res.status(500).send(error)); + } catch (err) { + res.json(err); + } + }; + return { fetchSingleTool }; +}; + +module.exports = bmToolController; diff --git a/src/controllers/dashBoardController.js b/src/controllers/dashBoardController.js index c9cdbd588..19e44af85 100644 --- a/src/controllers/dashBoardController.js +++ b/src/controllers/dashBoardController.js @@ -1,8 +1,9 @@ -const mongoose = require("mongoose"); -const path = require("path"); -const fs = require("fs/promises"); -const dashboardhelper = require("../helpers/dashboardhelper")(); -const emailSender = require("../utilities/emailSender"); +const path = require('path'); +const fs = require('fs/promises'); +const mongoose = require('mongoose'); +const dashboardhelper = require('../helpers/dashboardhelper')(); +const emailSender = require('../utilities/emailSender'); +const AIPrompt = require('../models/weeklySummaryAIPrompt'); const dashboardcontroller = function () { const dashboarddata = function (req, res) { @@ -11,22 +12,55 @@ const dashboardcontroller = function () { const snapshot = dashboardhelper.personaldetails(userId); snapshot.then((results) => { - res.send(results).status(200); + res.status(200).send(results); }); }; + const updateAIPrompt = function (req, res) { + if (req.body.requestor.role === 'Owner') { + AIPrompt.findOneAndUpdate({ _id: 'ai-prompt' }, { ...req.body, aIPromptText: req.body.aIPromptText }) + .then(() => { + res.status(200).send('Successfully saved AI prompt.'); + }).catch(error => res.status(500).send(error)); + } + }; + + const getAIPrompt = function (req, res) { + AIPrompt.findById({ _id: 'ai-prompt' }) + .then((result) => { + if (result) { + // If the GPT prompt exists, send it back. + res.status(200).send(result); + } else { + // If the GPT prompt does not exist, create it. + const defaultPrompt = { + _id: 'ai-prompt', + aIPromptText: "Please edit the following summary of my week's work. Make sure it is professionally written in 3rd person format.\nWrite it as only one paragraph. It must be only one paragraph. Keep it less than 500 words. Start the paragraph with 'This week'.\nMake sure the paragraph contains no links or URLs and write it in a tone that is matter-of-fact and without embellishment.\nDo not add flowery language, keep it simple and factual. Do not add a final summary sentence. Apply all this to the following:", + }; + AIPrompt.create(defaultPrompt) + .then((newResult) => { + res.status(200).send(newResult); + }) + .catch((creationError) => { + res.status(500).send(creationError); + }); + } + }) + .catch(error => res.status(500).send(error)); + }; + const monthlydata = function (req, res) { const userId = mongoose.Types.ObjectId(req.params.userId); const laborthismonth = dashboardhelper.laborthismonth( userId, req.params.fromDate, - req.params.toDate + req.params.toDate, ); laborthismonth.then((results) => { if (!results || results.length === 0) { const emptyresult = [ { - projectName: "", + projectName: '', timeSpent_hrs: 0, }, ]; @@ -42,10 +76,10 @@ const dashboardcontroller = function () { const laborthisweek = dashboardhelper.laborthisweek( userId, req.params.fromDate, - req.params.toDate + req.params.toDate, ); laborthisweek.then((results) => { - res.send(results).status(200); + res.status(200).send(results); }); }; @@ -63,7 +97,7 @@ const dashboardcontroller = function () { }); } }) - .catch((error) => res.status(400).send(error)); + .catch(error => res.status(400).send(error)); }; const orgData = function (req, res) { @@ -73,7 +107,7 @@ const dashboardcontroller = function () { .then((results) => { res.status(200).send(results[0]); }) - .catch((error) => res.status(400).send(error)); + .catch(error => res.status(400).send(error)); }; const getBugReportEmailBody = function ( @@ -85,7 +119,7 @@ const dashboardcontroller = function () { expected, actual, visual, - severity + severity, ) { const text = `New Bug Report From ${firstName} ${lastName}:
[Feature Name] Bug Title:
@@ -130,32 +164,32 @@ const dashboardcontroller = function () { expected, actual, visual, - severity + severity, ); try { emailSender( - "onecommunityglobal@gmail.com", + 'onecommunityglobal@gmail.com', `Bug Rport from ${firstName} ${lastName}`, emailBody, - email + email, ); - res.status(200).send("Success"); + res.status(200).send('Success'); } catch { - res.status(500).send("Failed"); + res.status(500).send('Failed'); } }; const suggestionData = { suggestion: [ - "Identify and remedy poor client and/or user service experiences", - "Identify bright spots and enhance positive service experiences", - "Make fundamental changes to our programs and/or operations", - "Inform the development of new programs/projects", - "Identify where we are less inclusive or equitable across demographic groups", - "Strengthen relationships with the people we serve", + 'Identify and remedy poor client and/or user service experiences', + 'Identify bright spots and enhance positive service experiences', + 'Make fundamental changes to our programs and/or operations', + 'Inform the development of new programs/projects', + 'Identify where we are less inclusive or equitable across demographic groups', + 'Strengthen relationships with the people we serve', "Understand people's needs and how we can help them achieve their goals", - "Other", + 'Other', ], field: [], }; @@ -164,42 +198,58 @@ const dashboardcontroller = function () { let fieldaaray = []; if (suggestionData.field.length) { fieldaaray = suggestionData.field.map( - (item) => `${item}
-${args[3][item]}
` + item => `${item}
+${args[3][item]}
`, ); } - const text = `New Suggestion: -Suggestion Category:
-${args[0]}
-Suggestion:
-${args[1]}
- ${fieldaaray.length > 0 ? fieldaaray : ""} -Wants Feedback:
-${args[2]}
-Thank you,
- One Community
${args[0]}
+ ⚹ Suggestion: +${args[1]}
+ ${fieldaaray.length > 0 ? fieldaaray : ''} + ⚹ Name of Suggester: +${args[3].firstName} ${args[3].lastName}
+ ⚹ Email of Suggester: +${args[4]}
+ ⚹ Wants Feedback: +${args[2]}
+ Thank you,Hi !
+ +This email is to let you know that ${foundUser.firstName} ${foundUser.lastName} has set their Blue Square Reason.
+ +Blue Square Reason : ${newReason.reason}
+Scheduled date for the Blue Square Reason: : ${newReason.date}
+ +Thank you,
+ One Community
Hi !
+ +This email is to let you know that ${foundUser.firstName} ${foundUser.lastName} has updated their Blue Square Reason.
+ +Updated Blue Square Reason : ${foundReason.reason}
+Scheduled date for the Blue Square Reason: : ${foundReason.date}
+ +Thank you,
+ One Community
The following task is available to review:
+${taskName}
+Thank you,
+One Community
`; + + return text; + }; + + const getRecipients = async function (myUserId) { + const recipients = []; + const user = await UserProfile.findById(myUserId); + const membership = await UserProfile.find({ + role: { $in: ['Administrator', 'Manager', 'Mentor'] }, + }); + membership.forEach((member) => { + if (member.teams.some(team => user.teams.includes(team))) { + recipients.push(member.email); + } + }); + return recipients; + }; + + const sendReviewReq = async function (req, res) { + const { myUserId, name, taskName } = req.body; + const emailBody = getReviewReqEmailBody(name, taskName); + const recipients = await getRecipients(myUserId); + + try { + emailSender( + recipients, + `Review Request from ${name}`, + emailBody, + null, + null, + ); + res.status(200).send('Success'); + } catch (err) { + res.status(500).send('Failed'); + } + }; + return { postTask, getTasks, @@ -862,8 +960,10 @@ const taskController = function (Task) { updateAllParents, deleteTaskByWBS, moveTask, - getTasksByUserList, + getTasksByUserId, getTasksForTeamsByUser, + updateTaskStatus, + sendReviewReq, }; }; diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index c82b9e5ce..9cd98036e 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -7,31 +7,46 @@ const teamcontroller = function (Team) { const getAllTeams = function (req, res) { Team.find({}) .sort({ teamName: 1 }) - .then(results => res.send(results).status(200)) - .catch(error => res.send(error).status(404)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const getTeamById = function (req, res) { const { teamId } = req.params; Team.findById(teamId) - .then(results => res.send(results).status(200)) - .catch(error => res.send(error).status(404)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const postTeam = async function (req, res) { if (!await hasPermission(req.body.requestor, 'postTeam')) { res.status(403).send({ error: 'You are not authorized to create teams.' }); return; } - const team = new Team(); + if (await Team.exists({ teamName: req.body.teamName })) { + res.status(403).send({ error: `Team Name "${req.body.teamName}" already exists` }); + return; + } + + const team = new Team(); team.teamName = req.body.teamName; - team.isACtive = req.body.isActive; + team.isActive = req.body.isActive; team.createdDatetime = Date.now(); team.modifiedDatetime = Date.now(); - team - .save() - .then(results => res.send(results).status(200)) + // Check if a team with the same name already exists + Team.findOne({ teamName: team.teamName }) + .then((existingTeam) => { + if (existingTeam) { + // If a team with the same name exists, return an error + res.status(400).send({ error: 'A team with this name already exists' }); + } else { + // If no team with the same name exists, save the new team + team.save() + .then(results => res.send(results).status(200)) + .catch(error => res.send(error).status(404)); + } + }) .catch(error => res.send(error).status(404)); }; const deleteTeam = async function (req, res) { @@ -49,7 +64,7 @@ const teamcontroller = function (Team) { const deleteteam = record.remove(); Promise.all([removeteamfromprofile, deleteteam]) - .then(res.status(200).send({ message: ' Team successfully deleted and user profiles updated' })) + .then(res.status(200).send({ message: 'Team successfully deleted and user profiles updated' })) .catch((errors) => { res.status(400).send(errors); }); @@ -70,14 +85,24 @@ const teamcontroller = function (Team) { res.status(400).send('No valid records found'); return; } + + const canEditTeamCode = req.body.requestor.role === 'Owner' + || req.body.requestor.permissions?.frontPermissions.includes('editTeamCode'); + + if (!canEditTeamCode) { + res.status(403).send('You are not authorized to edit team code.'); + return; + } + record.teamName = req.body.teamName; record.isActive = req.body.isActive; + record.teamCode = req.body.teamCode; record.createdDatetime = Date.now(); record.modifiedDatetime = Date.now(); record .save() - .then(results => res.status(201).send(results._id)) + .then(results => res.status(200).send(results._id)) .catch(errors => res.status(400).send(errors)); }); }; @@ -90,66 +115,40 @@ const teamcontroller = function (Team) { return; } - if ( - !req.params.teamId - || !mongoose.Types.ObjectId.isValid(req.params.teamId) - || !req.body.users - || req.body.users.length === 0 - ) { - res.status(400).send({ error: 'Invalid request' }); + const { teamId } = req.params; + + if (!teamId || !mongoose.Types.ObjectId.isValid(teamId)) { + res.status(400).send({ error: 'Invalid teamId' }); return; } // verify team exists + const targetTeam = await Team.findById(teamId); - Team.findById(req.params.teamId) - .then((team) => { - if (!team || team.length === 0) { - res.status(400).send({ error: 'Invalid team' }); - return; - } - const { users } = req.body; - const assignlist = []; - const unassignlist = []; - - users.forEach((element) => { - const { userId, operation } = element; - // if user's profile is stored in cache, clear it so when you visit their profile page it will be up to date - if(cache.hasCache(`user-${userId}`)) cache.removeCache(`user-${userId}`); - - if (operation === 'Assign') { - assignlist.push(userId); - } else { - unassignlist.push(userId); - } - }); + if (!targetTeam || targetTeam.length === 0) { + res.status(400).send({ error: 'Invalid team' }); + return; + } - const addTeamToUserProfile = userProfile - .updateMany({ _id: { $in: assignlist } }, { $addToSet: { teams: team._id } }) - .exec(); - const removeTeamFromUserProfile = userProfile - .updateMany({ _id: { $in: unassignlist } }, { $pull: { teams: team._id } }) - .exec(); - const addUserToTeam = Team.updateOne( - { _id: team._id }, - { $addToSet: { members: { $each: assignlist.map(userId => ({ userId })) } } }, - ).exec(); - const removeUserFromTeam = Team.updateOne( - { _id: team._id }, - { $pull: { members: { userId: { $in: unassignlist } } } }, - ).exec(); - - Promise.all([addTeamToUserProfile, removeTeamFromUserProfile, addUserToTeam, removeUserFromTeam]) - .then(() => { - res.status(200).send({ result: 'Done' }); - }) - .catch((error) => { - res.status(500).send({ error }); - }); - }) - .catch((error) => { - res.status(500).send({ error }); - }); + try { + const { userId, operation } = req.body; + + // if user's profile is stored in cache, clear it so when you visit their profile page it will be up to date + if (cache.hasCache(`user-${userId}`)) cache.removeCache(`user-${userId}`); + + + if (operation === 'Assign') { + await Team.findOneAndUpdate({ _id: teamId }, { $addToSet: { members: { userId } }, $set: { modifiedDatetime: Date.now() } }, { new: true }); + const newMember = await userProfile.findOneAndUpdate({ _id: userId }, { $addToSet: { teams: teamId } }, { new: true }); + res.status(200).send({ newMember }); + } else { + await Team.findOneAndUpdate({ _id: teamId }, { $pull: { members: { userId } }, $set: { modifiedDatetime: Date.now() } }); + await userProfile.findOneAndUpdate({ _id: userId }, { $pull: { teams: teamId } }, { new: true }); + res.status(200).send({ result: 'Delete Success' }); + } + } catch (error) { + res.status(500).send({ error }); + } }; const getTeamMembership = function (req, res) { diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index bea25d0a6..1cbab61c4 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -1,8 +1,9 @@ const moment = require('moment-timezone'); const mongoose = require('mongoose'); const { getInfringementEmailBody } = require('../helpers/userHelper')(); -const userProfile = require('../models/userProfile'); -const task = require('../models/task'); +const UserProfile = require('../models/userProfile'); +const Task = require('../models/task'); +const WBS = require('../models/wbs'); const emailSender = require('../utilities/emailSender'); const { hasPermission } = require('../utilities/permissions'); @@ -14,6 +15,13 @@ const formatSeconds = function (seconds) { return values.split(':'); }; +const isGeneralTimeEntry = function (type) { + if (type === undefined || type === 'default') { + return true; + } + return false; +}; + /** * * @param {*} firstName First name of the owner of the time entry that was modified @@ -24,9 +32,20 @@ const formatSeconds = function (seconds) { * @param {*} requestor The userProfile object of the person that modified the time entry * @returns {String} */ -const getEditedTimeEntryEmailBody = (firstName, lastName, email, originalTime, finalTime, requestor) => { - const formattedOriginal = moment.utc(originalTime * 1000).format('HH[ hours ]mm[ minutes]'); - const formattedFinal = moment.utc(finalTime * 1000).format('HH[ hours ]mm[ minutes]'); +const getEditedTimeEntryEmailBody = ( + firstName, + lastName, + email, + originalTime, + finalTime, + requestor, +) => { + const formattedOriginal = moment + .utc(originalTime * 1000) + .format('HH[ hours ]mm[ minutes]'); + const formattedFinal = moment + .utc(finalTime * 1000) + .format('HH[ hours ]mm[ minutes]'); return ` A time entry belonging to ${firstName} ${lastName} (${email}) was modified by ${requestor.firstName} ${requestor.lastName} (${requestor.email}). The entry's duration was changed from [${formattedOriginal}] to [${formattedFinal}] @@ -41,22 +60,41 @@ const getEditedTimeEntryEmailBody = (firstName, lastName, email, originalTime, f * @param {*} final Final time entry object * @returns {Void} */ -const notifyEditByEmail = async (personId, original, finalTime, final) => { +const notifyEditByEmail = async (personId, originalTime, finalTime, final) => { try { - const originalTime = original.totalSeconds; - const record = await userProfile.findById(personId); - const requestor = (personId !== final.requestor.requestorId) ? await userProfile.findById(final.requestor.requestorId) : record; - const emailBody = getEditedTimeEntryEmailBody(record.firstName, record.lastName, record.email, originalTime, finalTime, requestor); - emailSender('onecommunityglobal@gmail.com', `A Time Entry was Edited for ${record.firstName} ${record.lastName}`, emailBody); + const record = await UserProfile.findById(personId); + const requestor = personId !== final.requestor.requestorId + ? await UserProfile.findById(final.requestor.requestorId) + : record; + const emailBody = getEditedTimeEntryEmailBody( + record.firstName, + record.lastName, + record.email, + originalTime, + finalTime, + requestor, + ); + emailSender( + 'onecommunityglobal@gmail.com', + `A Time Entry was Edited for ${record.firstName} ${record.lastName}`, + emailBody, + ); } catch (error) { - throw new Error(`Failed to send email notification about the modification of time entry belonging to user with id ${personId}`); + throw new Error( + `Failed to send email notification about the modification of time entry belonging to user with id ${personId}`, + ); } }; -const notifyTaskOvertimeEmailBody = async (personId, taskName, estimatedHours, hoursLogged) => { +const notifyTaskOvertimeEmailBody = async ( + personId, + taskName, + estimatedHours, + hoursLogged, +) => { try { - const record = await userProfile.findById(personId); - const text = `Dear ${record.firstName}${record.lastName}, + const record = await UserProfile.findById(personId); + const text = `Dear ${record.firstName}${record.lastName},Oops, it looks like you have logged more hours than estimated for a task
Task Name : ${taskName}
Time Estimated : ${estimatedHours}
@@ -64,157 +102,249 @@ const notifyTaskOvertimeEmailBody = async (personId, taskName, estimatedHours, hPlease connect with your manager to explain what happened and submit a new hours estimation for completion.
Thank you,
One Community
`; - emailSender( - record.email, - 'Logged more hours than estimated for a task', - text, - 'onecommunityglobal@gmail.com', - null, + emailSender( + record.email, + 'Logged more hours than estimated for a task', + text, + 'onecommunityglobal@gmail.com', + null, + record.email, + null, ); } catch (error) { - console.log(`Failed to send email notification about the overtime for a task belonging to user with id ${personId}`); + console.log( + `Failed to send email notification about the overtime for a task belonging to user with id ${personId}`, + ); } }; -const checkTaskOvertime = async (timeentry, record, currentTask) => { +const checkTaskOvertime = async (timeentry, currentUser, currentTask) => { try { // send email notification if logged in hours exceeds estiamted hours for a task - if (currentTask.hoursLogged > currentTask.estimatedHours) { notifyTaskOvertimeEmailBody(timeentry.personId.toString(), currentTask.taskName, currentTask.estimatedHours, currentTask.hoursLogged); } + if (currentTask.hoursLogged > currentTask.estimatedHours) { + notifyTaskOvertimeEmailBody( + timeentry.personId.toString(), + currentTask.taskName, + currentTask.estimatedHours, + currentTask.hoursLogged, + ); + } } catch (error) { - console.log(`Failed to find task whose logged-in hours are more than estimated hours ${record.email}`); + console.log( + `Failed to find task whose logged-in hours are more than estimated hours for ${currentUser.email}`, + ); + } +}; + +// update timeentry with wbsId and taskId if projectId in the old timeentry is actually a taskId +const updateTaskIdInTimeEntry = async (id, timeEntry) => { + // if id is a taskId, then timeentry should have the parent wbsId and projectId for that task; + // if id is not a taskId, then it is a projectId, timeentry should have both wbsId and taskId to be null; + let taskId = null; + let wbsId = null; + let projectId = id; + const task = await Task.findById(id); + if (task) { + taskId = id; + ({ wbsId } = task); + const wbs = await WBS.findById(wbsId); + ({ projectId } = wbs); } + Object.assign(timeEntry, { taskId, wbsId, projectId }); }; const timeEntrycontroller = function (TimeEntry) { const editTimeEntry = async (req, res) => { + const { timeEntryId } = req.params; + + if (!timeEntryId) { + const error = 'ObjectId in request param is not in correct format'; + return res.status(400).send({ error }); + } + + if (!mongoose.Types.ObjectId.isValid(timeEntryId)) { + const error = 'ObjectIds are not correctly formed'; + return res.status(400).send({ error }); + } + + const { + personId, + hours: newHours = '00', + minutes: newMinutes = '00', + notes: newNotes, + isTangible: newIsTangible, + projectId: newProjectId, + wbsId: newWbsId, + taskId: newTaskId, + dateOfWork: newDateOfWork, + } = req.body; + + const isForAuthUser = personId === req.body.requestor.requestorId; + const isSameDayTimeEntry = moment().tz('America/Los_Angeles').format('YYYY-MM-DD') === newDateOfWork; + const canEdit = (await hasPermission(req.body.requestor, 'editTimeEntry')) || (isForAuthUser && isSameDayTimeEntry); + + if (!canEdit) { + const error = 'Unauthorized request'; + return res.status(403).send({ error }); + } + const session = await mongoose.startSession(); session.startTransaction(); + const type = req.body.entryType; + const isGeneralEntry = isGeneralTimeEntry(type); + try { - if (!req.params.timeEntryId) { - return res.status(400).send({ error: 'ObjectId in request param is not in correct format' }); + if (!timeEntryId) { + const error = 'ObjectId in request param is not in correct format'; + return res.status(400).send({ error }); } - if (!mongoose.Types.ObjectId.isValid(req.params.timeEntryId) || !mongoose.Types.ObjectId.isValid(req.body.projectId)) { - return res.status(400).send({ error: 'ObjectIds are not correctly formed' }); + if ( + !mongoose.Types.ObjectId.isValid(timeEntryId) + || ((isGeneralEntry || type === 'project') + && !mongoose.Types.ObjectId.isValid(newProjectId) + )) { + const error = 'ObjectIds are not correctly formed'; + return res.status(400).send({ error }); } // Get initial timeEntry by timeEntryId - const timeEntry = await TimeEntry.findById(req.params.timeEntryId); - + const timeEntry = await TimeEntry.findById(timeEntryId); if (!timeEntry) { - return res.status(400).send({ error: `No valid records found for ${req.params.timeEntryId}` }); + const error = `No valid records found for ${timeEntryId}`; + return res.status(400).send({ error }); } - if (!(await hasPermission(req.body.requestor, 'editTimeEntry') || timeEntry.personId.toString() === req.body.requestor.requestorId.toString())) { - return res.status(403).send({ error: 'Unauthorized request' }); - } - - const hours = req.body.hours ? req.body.hours : '00'; - const minutes = req.body.minutes ? req.body.minutes : '00'; - - const totalSeconds = moment.duration(`${hours}:${minutes}`).asSeconds(); + const newTotalSeconds = moment.duration({ hours: newHours, minutes: newMinutes }).asSeconds(); - if (timeEntry.isTangible === true && totalSeconds !== timeEntry.totalSeconds) { - notifyEditByEmail(timeEntry.personId.toString(), timeEntry, totalSeconds, req.body); + if (isGeneralEntry && timeEntry.isTangible && newIsTangible && newTotalSeconds !== timeEntry.totalSeconds) { + notifyEditByEmail( + timeEntry.personId.toString(), + timeEntry.totalSeconds, + newTotalSeconds, + req.body, + ); } - const initialSeconds = timeEntry.totalSeconds; - const initialProjectId = timeEntry.projectId; - const initialIsTangible = timeEntry.isTangible; - - timeEntry.notes = req.body.notes; - timeEntry.totalSeconds = totalSeconds; - timeEntry.isTangible = req.body.isTangible; - timeEntry.lastModifiedDateTime = moment().utc().toISOString(); - timeEntry.projectId = mongoose.Types.ObjectId(req.body.projectId); - timeEntry.dateOfWork = moment(req.body.dateOfWork).format('YYYY-MM-DD'); - - // Update the hoursLogged field of related tasks based on before and after timeEntries - // initialIsTangible is a bealoon value, req.body.isTangible is a string - // initialProjectId may be a task id or project id, so do not throw error. - try { - if (initialIsTangible === true) { - const initialTask = await task.findById(initialProjectId); - initialTask.hoursLogged -= (initialSeconds / 3600); - await initialTask.save(); + // update task data if project/task is changed + if (newTaskId === timeEntry.taskId && newProjectId === timeEntry.projectId) { + // when project/task is the same + const timeEntryTask = await Task.findById(newTaskId); + if (timeEntryTask) { + const timeEntryUser = await UserProfile.findById(personId); + if (timeEntry.isTangible) { + timeEntryTask.hoursLogged -= timeEntry.totalSeconds / 3600; + } + if (newIsTangible) { + timeEntryTask.hoursLogged += newTotalSeconds / 3600; + } + checkTaskOvertime(timeEntry, timeEntryUser, timeEntryTask); + await timeEntryTask.save(); } - - if (req.body.isTangible === true) { - const editedTask = await task.findById(req.body.projectId); - editedTask.hoursLogged += (totalSeconds / 3600); - await editedTask.save(); + } else { + // update oldtTimeEntryTask + const oldTimeEntryTask = await Task.findById(timeEntry.taskId); + if (oldTimeEntryTask && timeEntry.isTangible) { + oldTimeEntryTask.hoursLogged -= timeEntry.totalSeconds / 3600; + oldTimeEntryTask.save(); + } + // update newtTimeEntryTask + const newTimeEntryTask = await Task.findById(newTaskId); + if (newTimeEntryTask && newIsTangible) { + const timeEntryUser = await UserProfile.findById(personId); + newTimeEntryTask.hoursLogged += newTotalSeconds / 3600; + checkTaskOvertime(timeEntry, timeEntryUser, newTimeEntryTask); + await newTimeEntryTask.save(); } - } catch (error) { - console.log('Failed to find task by id'); } // Update edit history - if (initialSeconds !== totalSeconds + if ((isGeneralEntry || type === 'person') + && timeEntry.totalSeconds !== newTotalSeconds && timeEntry.isTangible - && req.body.requestor.requestorId === timeEntry.personId.toString() - && !await hasPermission(req.body.requestor, 'editTimeEntry') - ) { - const requestor = await userProfile.findById(req.body.requestor.requestorId); + && isForAuthUser + && !(await hasPermission(req.body.requestor, 'editTimeEntry')) + ) { + const requestor = await UserProfile.findById( + req.body.requestor.requestorId, + ); + requestor.timeEntryEditHistory.push({ date: moment().tz('America/Los_Angeles').toDate(), - initialSeconds, - newSeconds: totalSeconds, + initialSeconds: timeEntry.totalSeconds, + newSeconds: newTotalSeconds, }); - // Issue infraction if edit history contains more than 5 edits in the last year - let totalRecentEdits = 0; - - requestor.timeEntryEditHistory.forEach((edit) => { - if (moment().tz('America/Los_Angeles').diff(edit.date, 'days') <= 365) { - totalRecentEdits += 1; - } - }); + if (isGeneralEntry) { + // Issue infraction if edit history contains more than 5 edits in the last year + let totalRecentEdits = 0; - if (totalRecentEdits >= 5) { - requestor.infringements.push({ - date: moment().tz('America/Los_Angeles'), - description: `${totalRecentEdits} time entry edits in the last calendar year`, + requestor.timeEntryEditHistory.forEach((edit) => { + if ( + moment() + .tz('America/Los_Angeles') + .diff(edit.date, 'days') <= 365 + ) totalRecentEdits += 1; }); - emailSender('onecommunityglobal@gmail.com', `${requestor.firstName} ${requestor.lastName} was issued a blue square for for editing a time entry ${totalRecentEdits} times`, ` -- ${requestor.firstName} ${requestor.lastName} (${requestor.email}) was issued a blue square for editing their time entries ${totalRecentEdits} times - within the last calendar year. -
-- This is the ${totalRecentEdits}th edit within the past 365 days. -
- `); - - const emailInfringement = { - date: moment().tz('America/Los_Angeles').format('MMMM-DD-YY'), - description: `You edited your time entries ${totalRecentEdits} times within the last 365 days, exceeding the limit of 4 times per year you can edit them without penalty.`, - }; - - emailSender(requestor.email, 'You\'ve been issued a blue square for editing your time entry', getInfringementEmailBody(requestor.firstName, requestor.lastName, emailInfringement, requestor.infringements.length)); - } + if (totalRecentEdits >= 5) { + requestor.infringements.push({ + date: moment().tz('America/Los_Angeles'), + description: `${totalRecentEdits} time entry edits in the last calendar year`, + }); + emailSender( + 'onecommunityglobal@gmail.com', + `${requestor.firstName} ${requestor.lastName} was issued a blue square for for editing a time entry ${totalRecentEdits} times`, + ` ++ ${requestor.firstName} ${requestor.lastName} (${requestor.email}) was issued a blue square for editing their time entries ${totalRecentEdits} times + within the last calendar year. +
++ This is the ${totalRecentEdits}th edit within the past 365 days. +
+ `, + ); + + const emailInfringement = { + date: moment().tz('America/Los_Angeles').format('MMMM-DD-YY'), + description: `You edited your time entries ${totalRecentEdits} times within the last 365 days, exceeding the limit of 4 times per year you can edit them without penalty.`, + }; + + emailSender( + requestor.email, + "You've been issued a blue square for editing your time entry", + getInfringementEmailBody( + requestor.firstName, + requestor.lastName, + emailInfringement, + requestor.infringements.length, + ), + ); + } + } await requestor.save(); } - + timeEntry.notes = newNotes; + timeEntry.totalSeconds = newTotalSeconds; + timeEntry.isTangible = newIsTangible; + timeEntry.lastModifiedDateTime = moment().utc().toISOString(); + timeEntry.projectId = mongoose.Types.ObjectId(newProjectId); + timeEntry.wbsId = newWbsId ? mongoose.Types.ObjectId(newWbsId) : null; + timeEntry.taskId = newTaskId ? mongoose.Types.ObjectId(newTaskId) : null; + timeEntry.dateOfWork = moment(newDateOfWork).format('YYYY-MM-DD'); await timeEntry.save(); - res.status(200).send({ message: 'Successfully updated time entry' }); - - // checking if logged in hours exceed estimated time after timeentry edit for a task - const record = await userProfile.findById(timeEntry.personId.toString()); - const currentTask = await task.findById(req.body.projectId); - checkTaskOvertime(timeEntry, record, currentTask); + return res.status(200).send({ message: 'Successfully updated time entry' }); } catch (err) { await session.abortTransaction(); return res.status(400).send({ error: err.toString() }); } finally { session.endSession(); } - - return res.status(200).send(); }; const getAllTimeEnteries = function (req, res) { @@ -224,70 +354,107 @@ const timeEntrycontroller = function (TimeEntry) { } const items = []; records.forEach((element) => { - const timeentry = new TimeEntry(); - timeentry.personId = element.personId; - timeentry.projectId = element.projectId; - timeentry.dateOfWork = element.dateOfWork; - timeentry.timeSpent = moment('1900-01-01 00:00:00') - .add(element.totalSeconds, 'seconds') - .format('HH:mm:ss'); - timeentry.notes = element.notes; - timeentry.isTangible = element.isTangible; - items.push(timeentry); + const isGeneralEntry = isGeneralTimeEntry(element.entryType); + if (isGeneralEntry) { + const timeentry = new TimeEntry(); + timeentry.personId = element.personId; + timeentry.projectId = element.projectId; + timeentry.wbsId = element.wbsId; + timeentry.taskId = element.taskId; + timeentry.dateOfWork = element.dateOfWork; + timeentry.timeSpent = moment('1900-01-01 00:00:00') + .add(element.totalSeconds, 'seconds') + .format('HH:mm:ss'); + timeentry.notes = element.notes; + timeentry.isTangible = element.isTangible; + timeentry.entryType = 'default'; + items.push(timeentry); + } }); return res.json(items).status(200); }); }; const postTimeEntry = async function (req, res) { - if ( - !mongoose.Types.ObjectId.isValid(req.body.personId) - || !mongoose.Types.ObjectId.isValid(req.body.projectId) - || !req.body.dateOfWork + const isInvalid = !req.body.dateOfWork || !moment(req.body.dateOfWork).isValid() - || !req.body.timeSpent - || !req.body.isTangible - ) { - res.status(400).send({ error: 'Bad request' }); - return; + || !req.body.timeSpent; + + const returnErr = (result) => { + result.status(400).send({ error: 'Bad request' }); + }; + + switch (req.body.entryType) { + default: + if ( + !mongoose.Types.ObjectId.isValid(req.body.personId) + || !mongoose.Types.ObjectId.isValid(req.body.projectId) + || isInvalid + ) { + returnErr(res); + } + break; + case 'person': + if ( + !mongoose.Types.ObjectId.isValid(req.body.personId) || isInvalid + ) { + returnErr(res); + } + break; + case 'project': + if ( + !mongoose.Types.ObjectId.isValid(req.body.projectId) || isInvalid + ) { + returnErr(res); + } + break; + case 'team': + if ( + !mongoose.Types.ObjectId.isValid(req.body.teamId) || isInvalid + ) { + returnErr(res); + } + break; } - const timeentry = new TimeEntry(); + + const timeEntry = new TimeEntry(); const { dateOfWork, timeSpent } = req.body; - timeentry.personId = req.body.personId; - timeentry.projectId = req.body.projectId; - timeentry.dateOfWork = moment(dateOfWork).format('YYYY-MM-DD'); - timeentry.totalSeconds = moment.duration(timeSpent).asSeconds(); - timeentry.notes = req.body.notes; - timeentry.isTangible = req.body.isTangible; - timeentry.createdDateTime = moment().utc().toISOString(); - timeentry.lastModifiedDateTime = moment().utc().toISOString(); - - timeentry - .save() - .then((results) => { - res - .status(200) - .send({ message: `Time Entry saved with id as ${results._id}` }); - }) - .catch(error => res.status(400).send(error)); - - // Add this tangbile time entry to related task's hoursLogged - if (timeentry.isTangible === true) { - try { - const currentTask = await task.findById(req.body.projectId); - currentTask.hoursLogged += (timeentry.totalSeconds / 3600); - await currentTask.save(); - } catch (error) { - throw new Error('Failed to find the task by id'); + timeEntry.personId = req.body.personId; + timeEntry.projectId = req.body.projectId; + timeEntry.wbsId = req.body.wbsId; + timeEntry.taskId = req.body.taskId; + timeEntry.teamId = req.body.teamId; + timeEntry.dateOfWork = moment(dateOfWork).format('YYYY-MM-DD'); + timeEntry.totalSeconds = moment.duration(timeSpent).asSeconds(); + timeEntry.notes = req.body.notes; + timeEntry.isTangible = req.body.isTangible; + timeEntry.createdDateTime = moment().utc().toISOString(); + timeEntry.lastModifiedDateTime = moment().utc().toISOString(); + timeEntry.entryType = req.body.entryType; + + if (timeEntry.taskId) { + const timeEntryTask = await Task.findById(timeEntry.taskId); + const timeEntryUser = await UserProfile.findById(timeEntry.personId); + if (timeEntry.isTangible) { + timeEntryTask.hoursLogged += timeEntry.totalSeconds / 3600; } + checkTaskOvertime(timeEntry, timeEntryUser, timeEntryTask); + await timeEntryTask.save(); + } + + try { + return timeEntry + .save() + .then(results => res.status(200).send({ + message: `Time Entry saved with id as ${results._id}`, + })) + .catch(error => res.status(400).send(error)); + } catch (error) { + return res.status(500).send(error); } - // checking if logged in hours exceed estimated time after timeentry for a task - const record = await userProfile.findById(timeentry.personId.toString()); - const currentTask = await task.findById(req.body.projectId); - checkTaskOvertime(timeentry, record, currentTask); }; - const getTimeEntriesForSpecifiedPeriod = function (req, res) { + const getTimeEntriesForSpecifiedPeriod = async function (req, res) { if ( !req.params || !req.params.fromdate @@ -300,89 +467,35 @@ const timeEntrycontroller = function (TimeEntry) { return; } - const fromdate = moment(req.params.fromdate).tz('America/Los_Angeles').format('YYYY-MM-DD'); - const todate = moment(req.params.todate).tz('America/Los_Angeles').format('YYYY-MM-DD'); + const fromdate = moment(req.params.fromdate) + .tz('America/Los_Angeles') + .format('YYYY-MM-DD'); + const todate = moment(req.params.todate) + .tz('America/Los_Angeles') + .format('YYYY-MM-DD'); const { userId } = req.params; - TimeEntry.aggregate([ - { - $match: { - personId: mongoose.Types.ObjectId(userId), - dateOfWork: { $gte: fromdate, $lte: todate }, - }, - }, - { - $lookup: { - from: 'projects', - localField: 'projectId', - foreignField: '_id', - as: 'project', - }, - }, - { - $lookup: { - from: 'tasks', - localField: 'projectId', - foreignField: '_id', - as: 'task', - }, - }, - { - $project: { - _id: 1, - notes: 1, - isTangible: 1, - personId: 1, - projectId: 1, - lastModifiedDateTime: 1, - projectName: { - $arrayElemAt: [ - '$project.projectName', - 0, - ], - }, - taskName: { - $arrayElemAt: [ - '$task.taskName', - 0, - ], - }, - category: { - $arrayElemAt: [ - '$project.category', - 0, - ], - }, - classification: { - $arrayElemAt: [ - '$task.classification', - 0, - ], - }, - dateOfWork: 1, - hours: { - $floor: { - $divide: ['$totalSeconds', 3600], - }, - }, - minutes: { - $floor: { - $divide: [ - { $mod: ['$totalSeconds', 3600] }, - 60, - ], - }, - }, - }, - }, - { - $sort: { - lastModifiedDateTime: -1, - }, - }, - ]).then((results) => { + try { + const timeEntries = await TimeEntry.find({ + entryType: { $in: ['default', null] }, + personId: userId, + dateOfWork: { $gte: fromdate, $lte: todate }, + }).sort('-lastModifiedDateTime'); + + const results = await Promise.all(timeEntries.map(async (timeEntry) => { + timeEntry = { ...timeEntry.toObject() }; + const { projectId, taskId } = timeEntry; + if (!taskId) await updateTaskIdInTimeEntry(projectId, timeEntry); // if no taskId, then it might be old time entry data that didn't separate projectId with taskId + const hours = Math.floor(timeEntry.totalSeconds / 3600); + const minutes = Math.floor((timeEntry.totalSeconds % 3600) / 60); + Object.assign(timeEntry, { hours, minutes, totalSeconds: undefined }); + return timeEntry; + })); + res.status(200).send(results); - }).catch(error => res.status(400).send(error)); + } catch (error) { + res.status(400).send({ error }); + } }; const getTimeEntriesForUsersList = function (req, res) { @@ -390,6 +503,7 @@ const timeEntrycontroller = function (TimeEntry) { TimeEntry.find( { + entryType: { $in: ['default', null, 'person'] }, personId: { $in: users }, dateOfWork: { $gte: fromDate, $lte: toDate }, }, @@ -416,7 +530,9 @@ const timeEntrycontroller = function (TimeEntry) { }); res.status(200).send(data); }) - .catch(error => res.status(400).send(error)); + .catch((error) => { + res.status(400).send(error); + }); }; const getTimeEntriesForSpecifiedProject = function (req, res) { @@ -444,7 +560,9 @@ const timeEntrycontroller = function (TimeEntry) { .then((results) => { res.status(200).send(results); }) - .catch(error => res.status(400).send(error)); + .catch((error) => { + res.status(400).send(error); + }); }; const deleteTimeEntry = async function (req, res) { @@ -460,17 +578,33 @@ const timeEntrycontroller = function (TimeEntry) { return; } + if (record.entryType === 'project' || record.entryType === 'person' || record.entryType === 'team') { + record + .remove() + .then(() => { + res.status(200).send({ message: 'Successfully deleted' }); + }) + .catch((error) => { + res.status(500).send(error); + }); + return; + } + if ( record.personId.toString() === req.body.requestor.requestorId.toString() - || await hasPermission(req.body.requestor, 'deleteTimeEntry') + || (await hasPermission(req.body.requestor, 'deleteTimeEntry')) ) { // Revert this tangible timeEntry of related task's hoursLogged if (record.isTangible === true) { - task.findById(record.projectId) + Task + .findById(record.projectId) .then((currentTask) => { - currentTask.hoursLogged -= (record.totalSeconds / 3600); - currentTask.save(); + // If the time entry isn't related to a task (i.e. it's a project), then don't revert hours (Most likely pr team) + if (currentTask) { + currentTask.hoursLogged -= record.totalSeconds / 3600; + currentTask.save(); + } }) .catch((error) => { throw new Error(error); @@ -494,6 +628,117 @@ const timeEntrycontroller = function (TimeEntry) { }); }; + const getLostTimeEntriesForUserList = function (req, res) { + const { users, fromDate, toDate } = req.body; + + TimeEntry.find( + { + entryType: 'person', + personId: { $in: users }, + dateOfWork: { $gte: fromDate, $lte: toDate }, + }, + ' -createdDateTime', + ) + .populate('personId') + .sort({ lastModifiedDateTime: -1 }) + .then((results) => { + const data = []; + results.forEach((element) => { + const record = {}; + + record._id = element._id; + record.notes = element.notes; + record.isTangible = element.isTangible; + record.personId = element.personId; + record.firstName = element.personId + ? element.personId.firstName + : ''; + record.lastName = element.personId + ? element.personId.lastName + : ''; + record.dateOfWork = element.dateOfWork; + record.entryType = element.entryType; + [record.hours, record.minutes] = formatSeconds(element.totalSeconds); + data.push(record); + }); + res.status(200).send(data); + }) + .catch((error) => { + res.status(400).send(error); + }); + }; + + const getLostTimeEntriesForProjectList = function (req, res) { + const { projects, fromDate, toDate } = req.body; + + TimeEntry.find( + { + entryType: 'project', + projectId: { $in: projects }, + dateOfWork: { $gte: fromDate, $lte: toDate }, + }, + ' -createdDateTime', + ) + .populate('projectId') + .sort({ lastModifiedDateTime: -1 }) + .then((results) => { + const data = []; + results.forEach((element) => { + const record = {}; + record._id = element._id; + record.notes = element.notes; + record.isTangible = element.isTangible; + record.projectId = element.projectId ? element.projectId._id : ''; + record.projectName = element.projectId + ? element.projectId.projectName + : ''; + record.dateOfWork = element.dateOfWork; + record.entryType = element.entryType; + [record.hours, record.minutes] = formatSeconds(element.totalSeconds); + data.push(record); + }); + res.status(200).send(data); + }) + .catch((error) => { + res.status(400).send(error); + }); + }; + + const getLostTimeEntriesForTeamList = function (req, res) { + const { teams, fromDate, toDate } = req.body; + + TimeEntry.find( + { + entryType: 'team', + teamId: { $in: teams }, + dateOfWork: { $gte: fromDate, $lte: toDate }, + }, + ' -createdDateTime', + ) + .populate('teamId') + .sort({ lastModifiedDateTime: -1 }) + .then((results) => { + const data = []; + results.forEach((element) => { + const record = {}; + record._id = element._id; + record.notes = element.notes; + record.isTangible = element.isTangible; + record.teamId = element.teamId ? element.teamId._id : ''; + record.teamName = element.teamId + ? element.teamId.teamName + : ''; + record.dateOfWork = element.dateOfWork; + record.entryType = element.entryType; + [record.hours, record.minutes] = formatSeconds(element.totalSeconds); + data.push(record); + }); + res.status(200).send(data); + }) + .catch((error) => { + res.status(400).send(error); + }); + }; return { getAllTimeEnteries, @@ -504,6 +749,9 @@ const timeEntrycontroller = function (TimeEntry) { deleteTimeEntry, getTimeEntriesForSpecifiedProject, checkTaskOvertime, + getLostTimeEntriesForUserList, + getLostTimeEntriesForProjectList, + getLostTimeEntriesForTeamList, }; }; diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index a36b1c065..0e352654f 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -36,7 +36,15 @@ async function ValidatePassword(req, res) { return; } // Verify request is authorized by self or adminsitrator - if (!userId === requestor.requestorId && !await hasPermission(req.body.requestor, 'updatePassword')) { + if (userId !== requestor.requestorId && !await hasPermission(req.body.requestor, 'updatePassword')) { + res.status(403).send({ + error: "You are unauthorized to update this user's password", + }); + return; + } + + // Verify request is authorized by self or adminsitrator + if (userId === requestor.requestorId || !await hasPermission(req.body.requestor, 'updatePassword')) { res.status(403).send({ error: "You are unauthorized to update this user's password", }); @@ -60,27 +68,25 @@ const userProfileController = function (UserProfile) { UserProfile.find( {}, - "_id firstName lastName role weeklycommittedHours email permissions isActive reactivationDate createdDate endDate" + '_id firstName lastName role weeklycommittedHours email permissions isActive reactivationDate createdDate endDate', ) .sort({ lastName: 1, }) .then((results) => { if (!results) { - if (cache.getCache("allusers")) { - const getData = JSON.parse(cache.getCache("allusers")); + if (cache.getCache('allusers')) { + const getData = JSON.parse(cache.getCache('allusers')); res.status(200).send(getData); return; - }else{ - res.status(500).send({ error: "User result was invalid" }); - return; } + res.status(500).send({ error: 'User result was invalid' }); + return; } - cache.setCache("allusers", JSON.stringify(results)); + cache.setCache('allusers', JSON.stringify(results)); res.status(200).send(results); }) - .catch((error) => res.status(404).send(error)); - + .catch(error => res.status(404).send(error)); }; const getProjectMembers = async function (req, res) { @@ -94,14 +100,14 @@ const userProfileController = function (UserProfile) { $in: [req.params.projectId], }, }, - "_id firstName email", + '_id firstName email', (err, profiles) => { if (err) { - res.status(404).send("Error finding user profiles"); + res.status(404).send('Error finding user profiles'); return; } res.json(profiles); - } + }, ); }; @@ -120,7 +126,7 @@ const userProfileController = function (UserProfile) { const userByEmail = await UserProfile.findOne({ email: { $regex: escapeRegex(req.body.email), - $options: "i", + $options: 'i', }, }); @@ -128,8 +134,8 @@ const userProfileController = function (UserProfile) { if (userByEmail) { res.status(400).send({ error: - "That email address is already in use. Please choose another email address.", - type: "email", + 'That email address is already in use. Please choose another email address.', + type: 'email', }); return; } @@ -176,8 +182,8 @@ const userProfileController = function (UserProfile) { if (userByPhoneNumber) { res.status(400).send({ error: - "That phone number is already in use. Please choose another number.", - type: "phoneNumber", + 'That phone number is already in use. Please choose another number.', + type: 'phoneNumber', }); return; } @@ -191,8 +197,8 @@ const userProfileController = function (UserProfile) { if (userDuplicateName && !req.body.allowsDuplicateName) { res.status(400).send({ error: - "That name is already in use. Please confirm if you want to use this name.", - type: "name", + 'That name is already in use. Please confirm if you want to use this name.', + type: 'name', }); return; } @@ -219,15 +225,15 @@ const userProfileController = function (UserProfile) { up.projects = Array.from(new Set(req.body.projects)); up.createdDate = req.body.createdDate; up.email = req.body.email; - up.weeklySummaries = req.body.weeklySummaries || [{ summary: "" }]; + up.weeklySummaries = req.body.weeklySummaries || [{ summary: '' }]; up.weeklySummariesCount = req.body.weeklySummariesCount || 0; up.weeklySummaryOption = req.body.weeklySummaryOption; - up.mediaUrl = req.body.mediaUrl || ""; - up.collaborationPreference = req.body.collaborationPreference || ""; - up.timeZone = req.body.timeZone || "America/Los_Angeles"; + up.mediaUrl = req.body.mediaUrl || ''; + up.collaborationPreference = req.body.collaborationPreference || ''; + up.timeZone = req.body.timeZone || 'America/Los_Angeles'; up.location = req.body.location; up.permissions = req.body.permissions; - up.bioPosted = req.body.bioPosted || "default"; + up.bioPosted = req.body.bioPosted || 'default'; up.isFirstTimelog = true; up.actualEmail = req.body.actualEmail; @@ -250,11 +256,11 @@ const userProfileController = function (UserProfile) { lastName: up.lastName, email: up.email, }; - const allUserCache = JSON.parse(cache.getCache("allusers")); + const allUserCache = JSON.parse(cache.getCache('allusers')); allUserCache.push(userCache); - cache.setCache("allusers", JSON.stringify(allUserCache)); + cache.setCache('allusers', JSON.stringify(allUserCache)); }) - .catch((error) => res.status(501).send(error)); + .catch(error => res.status(501).send(error)); }; const putUserProfile = async function (req, res) { @@ -265,12 +271,13 @@ const userProfileController = function (UserProfile) { || req.body.requestor.requestorId === userid ) ); - - const canEditTeamCode = req.body.requestor.role === "Owner" || - req.body.requestor.permissions?.frontPermissions.includes("editTeamCode"); + + const canEditTeamCode = req.body.requestor.role === 'Owner' + || req.body.requestor.role === 'Administrator' + || req.body.requestor.permissions?.frontPermissions.includes('editTeamCode'); if (!isRequestorAuthorized) { - res.status(403).send("You are not authorized to update this user"); + res.status(403).send('You are not authorized to update this user'); return; } @@ -282,7 +289,7 @@ const userProfileController = function (UserProfile) { cache.removeCache(`user-${userid}`); UserProfile.findById(userid, async (err, record) => { if (err || !record) { - res.status(404).send("No valid records found"); + res.status(404).send('No valid records found'); return; } // validate userprofile pic @@ -301,8 +308,7 @@ const userProfileController = function (UserProfile) { : []; record.jobTitle = req.body.jobTitle; record.emailPubliclyAccessible = req.body.emailPubliclyAccessible; - record.phoneNumberPubliclyAccessible = - req.body.phoneNumberPubliclyAccessible; + record.phoneNumberPubliclyAccessible = req.body.phoneNumberPubliclyAccessible; record.profilePic = req.body.profilePic; record.firstName = req.body.firstName; @@ -324,24 +330,25 @@ const userProfileController = function (UserProfile) { record.isVisible = req.body.isVisible || false; record.isRehireable = req.body.isRehireable || false; record.totalIntangibleHrs = req.body.totalIntangibleHrs; - record.bioPosted = req.body.bioPosted || "default"; + record.bioPosted = req.body.bioPosted || 'default'; record.isFirstTimelog = req.body.isFirstTimelog; + record.teamCode = req.body.teamCode; - if(!canEditTeamCode && record.teamCode !== req.body.teamCode){ - res.status(403).send("You are not authorized to edit team code."); + if (!canEditTeamCode && record.teamCode !== req.body.teamCode) { + res.status(403).send('You are not authorized to edit team code.'); return; } record.teamCode = req.body.teamCode; // find userData in cache - const isUserInCache = cache.hasCache("allusers"); + const isUserInCache = cache.hasCache('allusers'); let allUserData; let userData; let userIdx; if (isUserInCache) { - allUserData = JSON.parse(cache.getCache("allusers")); - userIdx = allUserData.findIndex((users) => users._id === userid); + allUserData = JSON.parse(cache.getCache('allusers')); + userIdx = allUserData.findIndex(users => users._id === userid); userData = allUserData[userIdx]; } if (await hasPermission(req.body.requestor, 'putUserProfileImportantInfo')) { @@ -356,11 +363,11 @@ const userProfileController = function (UserProfile) { // If their last update was made today, remove that const lasti = record.weeklycommittedHoursHistory.length - 1; const lastChangeDate = moment( - record.weeklycommittedHoursHistory[lasti].dateChanged + record.weeklycommittedHoursHistory[lasti].dateChanged, ); const now = moment(); - if (lastChangeDate.isSame(now, "day")) { + if (lastChangeDate.isSame(now, 'day')) { record.weeklycommittedHoursHistory.pop(); } @@ -373,8 +380,7 @@ const userProfileController = function (UserProfile) { record.weeklycommittedHoursHistory.push(newEntry); } - record.missedHours = - req.body.role === "Core Team" ? req.body?.missedHours ?? 0 : 0; + record.missedHours = req.body.role === 'Core Team' ? req.body?.missedHours ?? 0 : 0; record.adminLinks = req.body.adminLinks; record.teams = Array.from(new Set(req.body.teams)); record.projects = Array.from(new Set(req.body.projects)); @@ -406,8 +412,7 @@ const userProfileController = function (UserProfile) { record.weeklycommittedHoursHistory.push(newEntry); } // then also change the first committed history (index 0) - record.weeklycommittedHoursHistory[0].dateChanged = - record.createdDate; + record.weeklycommittedHoursHistory[0].dateChanged = record.createdDate; } record.bioPosted = req.body.bioPosted || 'default'; @@ -422,7 +427,7 @@ const userProfileController = function (UserProfile) { userData.endDate = record.endDate.toISOString(); } } else { - record.set("endDate", undefined, { strict: false }); + record.set('endDate', undefined, { strict: false }); } if (isUserInCache) { userData.role = record.role; @@ -444,7 +449,7 @@ const userProfileController = function (UserProfile) { results.infringements, results.firstName, results.lastName, - results.email + results.email, ); res.status(200).json({ _id: record._id, @@ -453,10 +458,10 @@ const userProfileController = function (UserProfile) { // update alluser cache if we have cache if (isUserInCache) { allUserData.splice(userIdx, 1, userData); - cache.setCache("allusers", JSON.stringify(allUserData)); + cache.setCache('allusers', JSON.stringify(allUserData)); } }) - .catch((error) => res.status(400).send(error)); + .catch(error => res.status(400).send(error)); }); }; @@ -601,15 +606,15 @@ const userProfileController = function (UserProfile) { const { userId } = req.params; const { key, value } = req.body; - if (key === "teamCode") { - const canEditTeamCode = req.body.requestor.role === "Owner" || - req.body.requestor.permissions?.frontPermissions.includes("editTeamCode"); + if (key === 'teamCode') { + const canEditTeamCode = req.body.requestor.role === 'Owner' + || req.body.requestor.role === 'Administrator' + || req.body.requestor.permissions?.frontPermissions.includes('editTeamCode'); - if(!canEditTeamCode){ - res.status(403).send("You are not authorized to edit team code."); + if (!canEditTeamCode) { + res.status(403).send('You are not authorized to edit team code.'); return; } - } // remove user from cache, it should be loaded next time @@ -648,25 +653,26 @@ const userProfileController = function (UserProfile) { error: 'One of more required fields are missing', }); } - // Verify request is authorized by self or adminsitrator - if (!userId === requestor.requestorId && !await hasPermission(req.body.requestor, 'updatePassword')) { - return res.status(403).send({ - error: "You are unauthorized to update this user's password", - }); - } + // Check if the requestor has the permission to update passwords. + const hasUpdatePasswordPermission = await hasPermission(requestor.role, 'updatePassword'); - if (canRequestorUpdateUser(requestor.requestorId, userId)) { - return res.status(403).send({ - error: "You are unauthorized to update this user's password", - }); + // If the requestor is updating their own password, allow them to proceed. + if (userId === requestor.requestorId) { + console.log('Requestor is updating their own password'); + } + // Else if they're updating someone else's password, they need the 'updatePassword' permission. + else if (!hasUpdatePasswordPermission) { + console.log("Requestor is trying to update someone else's password but lacks the 'updatePassword' permission"); + return res.status(403).send({ + error: "You are unauthorized to update this user's password", + }); } // Verify new and confirm new password are correct - if (req.body.newpassword !== req.body.confirmnewpassword) { - res.status(400).send({ - error: 'New and confirm new passwords are not same', - }); + return res.status(400).send({ + error: 'New and confirm new passwords are not the same', + }); } // Verify old and new passwords are not same diff --git a/src/controllers/wbsController.js b/src/controllers/wbsController.js index fa7f4427f..04070eaf9 100644 --- a/src/controllers/wbsController.js +++ b/src/controllers/wbsController.js @@ -1,4 +1,7 @@ +const mongoose = require('mongoose'); const { hasPermission } = require('../utilities/permissions'); +const Project = require('../models/project'); +const Task = require('../models/task'); const wbsController = function (WBS) { const getAllWBS = function (req, res) { @@ -68,12 +71,32 @@ const wbsController = function (WBS) { .catch(error => res.status(404).send(error)); }; + const getWBSByUserId = async function (req, res) { + const { userId } = req.params; + try { + const result = await Task.aggregate() + .match({ 'resources.userID': mongoose.Types.ObjectId(userId) }) + .project('wbsId -_id') + .group({ _id: '$wbsId' }) + .lookup({ + from: 'wbs', localField: '_id', foreignField: '_id', as: 'wbs', + }) + .unwind('wbs') + .replaceRoot('wbs'); + + res.status(200).send(result); + } catch (error) { + res.status(404).send(error); + } + }; + return { postWBS, deleteWBS, getAllWBS, getWBS, getWBSById, + getWBSByUserId, }; }; diff --git a/src/cronjobs/userProfileJobs.js b/src/cronjobs/userProfileJobs.js index f3903c057..8bc94c0f1 100644 --- a/src/cronjobs/userProfileJobs.js +++ b/src/cronjobs/userProfileJobs.js @@ -5,7 +5,8 @@ const userhelper = require('../helpers/userHelper')(); const userProfileJobs = () => { const allUserProfileJobs = new CronJob( - '1 0 * * *', // Every day, 1 minute past midnight (PST). + // '* * * * *', // Comment out for testing. Run Every minute. + '1 0 * * 0', // Every Sunday, 1 minute past midnight. async () => { const SUNDAY = 0; if (moment().tz('America/Los_Angeles').day() === SUNDAY) { diff --git a/src/helpers/dashboardhelper.js b/src/helpers/dashboardhelper.js index 34d464583..8df4f6c9d 100644 --- a/src/helpers/dashboardhelper.js +++ b/src/helpers/dashboardhelper.js @@ -1,26 +1,27 @@ -const moment = require('moment-timezone'); -const mongoose = require('mongoose'); -const userProfile = require('../models/userProfile'); -const timeentry = require('../models/timeentry'); -const myTeam = require('../helpers/helperModels/myTeam'); +const moment = require("moment-timezone"); +const mongoose = require("mongoose"); +const userProfile = require("../models/userProfile"); +const timeentry = require("../models/timeentry"); +const myTeam = require("../helpers/helperModels/myTeam"); +const team = require("../models/team"); const dashboardhelper = function () { const personaldetails = function (userId) { return userProfile.findById( userId, - '_id firstName lastName role profilePic badgeCollection', + "_id firstName lastName role profilePic badgeCollection" ); }; const getOrgData = async function () { const pdtstart = moment() - .tz('America/Los_Angeles') - .startOf('week') - .format('YYYY-MM-DD'); + .tz("America/Los_Angeles") + .startOf("week") + .format("YYYY-MM-DD"); const pdtend = moment() - .tz('America/Los_Angeles') - .endOf('week') - .format('YYYY-MM-DD'); + .tz("America/Los_Angeles") + .endOf("week") + .format("YYYY-MM-DD"); /** * Previous aggregate pipeline had two issues: @@ -39,35 +40,45 @@ const dashboardhelper = function () { $gte: 1, }, role: { - $ne: 'Mentor', + $ne: "Mentor", }, }, }, { $lookup: { - from: 'timeEntries', - localField: '_id', - foreignField: 'personId', - as: 'timeEntryData', + from: "timeEntries", + localField: "_id", + foreignField: "personId", + as: "timeEntryData", }, }, { $project: { - personId: '$_id', + personId: "$_id", name: 1, weeklycommittedHours: 1, role: 1, timeEntryData: { $filter: { - input: '$timeEntryData', - as: 'timeentry', + input: "$timeEntryData", + as: "timeentry", cond: { $and: [ { - $gte: ['$$timeentry.dateOfWork', pdtstart], + $gte: ["$$timeentry.dateOfWork", pdtstart], }, { - $lte: ['$$timeentry.dateOfWork', pdtend], + $lte: ["$$timeentry.dateOfWork", pdtend], + }, + { + $not: [ + { + $in: [ + "$$timeentry.entryType", + ["person", "team", "project"], + ], + }, + ], }, ], }, @@ -77,7 +88,7 @@ const dashboardhelper = function () { }, { $unwind: { - path: '$timeEntryData', + path: "$timeEntryData", preserveNullAndEmptyArrays: true, }, }, @@ -88,27 +99,27 @@ const dashboardhelper = function () { totalSeconds: { $cond: [ { - $gte: ['$timeEntryData.totalSeconds', 0], + $gte: ["$timeEntryData.totalSeconds", 0], }, - '$timeEntryData.totalSeconds', + "$timeEntryData.totalSeconds", 0, ], }, tangibletime: { $cond: [ { - $eq: ['$timeEntryData.isTangible', true], + $eq: ["$timeEntryData.isTangible", true], }, - '$timeEntryData.totalSeconds', + "$timeEntryData.totalSeconds", 0, ], }, intangibletime: { $cond: [ { - $eq: ['$timeEntryData.isTangible', false], + $eq: ["$timeEntryData.isTangible", false], }, - '$timeEntryData.totalSeconds', + "$timeEntryData.totalSeconds", 0, ], }, @@ -117,17 +128,17 @@ const dashboardhelper = function () { { $group: { _id: { - personId: '$personId', - weeklycommittedHours: '$weeklycommittedHours', + personId: "$personId", + weeklycommittedHours: "$weeklycommittedHours", }, time_hrs: { - $sum: { $divide: ['$totalSeconds', 3600] }, + $sum: { $divide: ["$totalSeconds", 3600] }, }, tangibletime_hrs: { - $sum: { $divide: ['$tangibletime', 3600] }, + $sum: { $divide: ["$tangibletime", 3600] }, }, intangibletime_hrs: { - $sum: { $divide: ['$intangibletime', 3600] }, + $sum: { $divide: ["$intangibletime", 3600] }, }, }, }, @@ -135,15 +146,19 @@ const dashboardhelper = function () { $group: { _id: 0, memberCount: { $sum: 1 }, - totalweeklycommittedHours: { $sum: '$_id.weeklycommittedHours' }, + totalweeklycommittedHours: { $sum: "$_id.weeklycommittedHours" }, + totalweeklycommittedHours: { $sum: "$_id.weeklycommittedHours" }, totaltime_hrs: { - $sum: '$time_hrs', + $sum: "$time_hrs", + $sum: "$time_hrs", }, totaltangibletime_hrs: { - $sum: '$tangibletime_hrs', + $sum: "$tangibletime_hrs", + $sum: "$tangibletime_hrs", }, totalintangibletime_hrs: { - $sum: '$intangibletime_hrs', + $sum: "$intangibletime_hrs", + $sum: "$intangibletime_hrs", }, }, }, @@ -152,264 +167,415 @@ const dashboardhelper = function () { return output; }; - const getLeaderboard = function (userId) { + const getLeaderboard = async function (userId) { const userid = mongoose.Types.ObjectId(userId); + const userById = await userProfile + .findOne({ _id: userid, isActive: true }, { role: 1 }) + .then((res) => res) + .catch((e) => {}); + + if (userById == null) return null; + const userRole = userById.role; const pdtstart = moment() - .tz('America/Los_Angeles') - .startOf('week') - .format('YYYY-MM-DD'); + .tz("America/Los_Angeles") + .startOf("week") + .format("YYYY-MM-DD"); + const pdtend = moment() - .tz('America/Los_Angeles') - .endOf('week') - .format('YYYY-MM-DD'); - return myTeam.aggregate([ - { - $match: { - _id: userid, - }, - }, - { - $unwind: '$myteam', - }, - { - $project: { - _id: 0, - role: 1, - personId: '$myteam._id', - name: '$myteam.fullName', - }, - }, - { - $lookup: { - from: 'userProfiles', - localField: 'personId', - foreignField: '_id', - as: 'persondata', - }, - }, - { - $match: { - // leaderboard user roles hierarchy - $or: [ - { - role: { $in: ['Owner', 'Core Team'] }, - }, - { - $and: [ - { - role: 'Administrator', - }, - { 'persondata.0.role': { $nin: ['Owner', 'Administrator'] } }, - ] - }, - { - $and: [ - { - role: { $in: ['Manager', 'Mentor'] }, - }, - { - 'persondata.0.role': { - $nin: ['Manager', 'Mentor', 'Core Team', 'Administrator', 'Owner'], - }, - }, - ], - }, - { 'persondata.0._id': userId }, - { 'persondata.0.role': 'Volunteer' }, - { 'persondata.0.isVisible': true }, - ], - }, - }, - { - $project: { - personId: 1, - name: 1, - role: { - $arrayElemAt: ['$persondata.role', 0], - }, - isVisible: { - $arrayElemAt: ['$persondata.isVisible', 0], - }, - hasSummary: { - $ne: [ - { - $arrayElemAt: [ - { - $arrayElemAt: ['$persondata.weeklySummaries.summary', 0], - }, - 0, - ], - }, - '', - ], - }, - weeklycommittedHours: { - $sum: [ - { - $arrayElemAt: ['$persondata.weeklycommittedHours', 0], - }, - { - $ifNull: [{ $arrayElemAt: ['$persondata.missedHours', 0] }, 0], - }, - ], - }, - }, - }, - { - $lookup: { - from: 'timeEntries', - localField: 'personId', - foreignField: 'personId', - as: 'timeEntryData', - }, - }, - { - $project: { - personId: 1, - name: 1, - role: 1, - isVisible: 1, - hasSummary: 1, - weeklycommittedHours: 1, - timeEntryData: { - $filter: { - input: '$timeEntryData', - as: 'timeentry', - cond: { - $and: [ - { - $gte: ['$$timeentry.dateOfWork', pdtstart], - }, - { - $lte: ['$$timeentry.dateOfWork', pdtend], - }, - ], - }, - }, - }, - }, - }, - { - $unwind: { - path: '$timeEntryData', - preserveNullAndEmptyArrays: true, - }, - }, - { - $project: { - personId: 1, - name: 1, - role: 1, - isVisible: 1, - hasSummary: 1, - weeklycommittedHours: 1, - totalSeconds: { - $cond: [ - { - $gte: ['$timeEntryData.totalSeconds', 0], - }, - '$timeEntryData.totalSeconds', - 0, - ], - }, - isTangible: { - $cond: [ - { - $gte: ['$timeEntryData.totalSeconds', 0], - }, - '$timeEntryData.isTangible', - false, - ], - }, - }, - }, - { - $addFields: { - tangibletime: { - $cond: [ - { - $eq: ['$isTangible', true], - }, - '$totalSeconds', - 0, - ], - }, - intangibletime: { - $cond: [ - { - $eq: ['$isTangible', false], - }, - '$totalSeconds', - 0, - ], - }, - }, - }, - { - $group: { - _id: { - personId: '$personId', - weeklycommittedHours: '$weeklycommittedHours', - name: '$name', - role: '$role', - isVisible: '$isVisible', - hasSummary: '$hasSummary', - }, - totalSeconds: { - $sum: '$totalSeconds', - }, - tangibletime: { - $sum: '$tangibletime', - }, - intangibletime: { - $sum: '$intangibletime', - }, - }, - }, - { - $project: { - _id: 0, - personId: '$_id.personId', - name: '$_id.name', - role: '$_id.role', - isVisible: '$_id.isVisible', - hasSummary: '$_id.hasSummary', - weeklycommittedHours: '$_id.weeklycommittedHours', - totaltime_hrs: { - $divide: ['$totalSeconds', 3600], - }, - totaltangibletime_hrs: { - $divide: ['$tangibletime', 3600], - }, - totalintangibletime_hrs: { - $divide: ['$intangibletime', 3600], - }, - percentagespentintangible: { - $cond: [ - { - $eq: ['$totalSeconds', 0], - }, - 0, - { - $multiply: [ - { - $divide: ['$tangibletime', '$totalSeconds'], - }, - 100, - ], - }, - ], - }, - }, - }, - { - $sort: { - totaltangibletime_hrs: -1, - name: 1, - role: 1, - }, + .tz("America/Los_Angeles") + .endOf("week") + .format("YYYY-MM-DD"); + + let teamMemberIds = [userid]; + let teamMembers = []; + + if ( + userRole != "Administrator" && + userRole != "Owner" && + userRole != "Core Team" + ) { + // Manager , Mentor , Volunteer ... , Show only team members + const teamsResult = await team + .find({ "members.userId": { $in: [userid] } }, { members: 1 }) + .then((res) => res) + .catch((e) => {}); + + teamsResult.map((_myTeam) => { + _myTeam.members.map((teamMember) => { + if (!teamMember.userId.equals(userid)) + teamMemberIds.push(teamMember.userId); + }); + }); + + teamMembers = await userProfile + .find( + { _id: { $in: teamMemberIds }, isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + isVisible: 1, + weeklycommittedHours: 1, + weeklySummaries: 1, + timeOffFrom: 1, + timeOffTill: 1, + } + ) + .then((res) => res) + .catch((e) => {}); + } else { + // 'Core Team', 'Owner' , 'Admin' //Show All users + teamMembers = await userProfile + .find( + { isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + isVisible: 1, + weeklycommittedHours: 1, + weeklySummaries: 1, + timeOffFrom: 1, + timeOffTill: 1, + } + ) + .then((res) => res) + .catch((e) => {}); + } + + teamMemberIds = teamMembers.map((member) => member._id); + + const timeEntries = await timeentry.find({ + dateOfWork: { + $gte: pdtstart, + $lte: pdtend, }, - ]); + personId: { $in: teamMemberIds }, + }); + + const timeEntryByPerson = {}; + timeEntries.map((timeEntry) => { + const personIdStr = timeEntry.personId.toString(); + + if (timeEntryByPerson[personIdStr] == null) { + timeEntryByPerson[personIdStr] = { + tangibleSeconds: 0, + intangibleSeconds: 0, + totalSeconds: 0, + }; + } + + if (timeEntry.isTangible === true) { + timeEntryByPerson[personIdStr].tangibleSeconds += + timeEntry.totalSeconds; + } else { + timeEntryByPerson[personIdStr].intangibleSeconds += + timeEntry.totalSeconds; + } + + timeEntryByPerson[personIdStr].totalSeconds += timeEntry.totalSeconds; + }); + + const leaderBoardData = []; + teamMembers.map((teamMember) => { + const obj = { + personId: teamMember._id, + role: teamMember.role, + name: `${teamMember.firstName} ${teamMember.lastName}`, + isVisible: teamMember.isVisible, + hasSummary: + teamMember.weeklySummaries?.length > 0 + ? teamMember.weeklySummaries[0].summary != "" + : false, + weeklycommittedHours: teamMember.weeklycommittedHours, + totaltangibletime_hrs: + timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds / + 3600 || 0, + totalintangibletime_hrs: + timeEntryByPerson[teamMember._id.toString()]?.intangibleSeconds / + 3600 || 0, + totaltime_hrs: + timeEntryByPerson[teamMember._id.toString()]?.totalSeconds / 3600 || + 0, + percentagespentintangible: + timeEntryByPerson[teamMember._id.toString()] && + timeEntryByPerson[teamMember._id.toString()]?.totalSeconds != 0 && + timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds != 0 + ? (timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds / + timeEntryByPerson[teamMember._id.toString()]?.totalSeconds) * + 100 + : 0, + timeOffFrom: teamMember.timeOffFrom || null, + timeOffTill: teamMember.timeOffTill || null, + }; + leaderBoardData.push(obj); + }); + + const sortedLBData = leaderBoardData.sort((a, b) => { + // Sort by totaltangibletime_hrs in descending order + if (b.totaltangibletime_hrs !== a.totaltangibletime_hrs) { + return b.totaltangibletime_hrs - a.totaltangibletime_hrs; + } + + // Then sort by name in ascending order + if (a.name !== b.name) { + return a.name.localeCompare(b.name); + } + + // Finally, sort by role in ascending order + return a.role.localeCompare(b.role); + }); + + return sortedLBData; + + // return myTeam.aggregate([ + // { + // $match: { + // _id: userid, + // }, + // }, + // { + // $unwind: '$myteam', + // }, + // { + // $project: { + // _id: 0, + // role: 1, + // personId: '$myteam._id', + // name: '$myteam.fullName', + // }, + // }, + // { + // $lookup: { + // from: 'userProfiles', + // localField: 'personId', + // foreignField: '_id', + // as: 'persondata', + // }, + // }, + // { + // $match: { + // // leaderboard user roles hierarchy + // $or: [ + // { + // role: { $in: ['Owner', 'Core Team'] }, + // }, + // { + // $and: [ + // { + // role: 'Administrator', + // }, + // { 'persondata.0.role': { $nin: ['Owner', 'Administrator'] } }, + // ], + // }, + // { + // $and: [ + // { + // role: { $in: ['Manager', 'Mentor'] }, + // }, + // { + // 'persondata.0.role': { + // $nin: ['Manager', 'Mentor', 'Core Team', 'Administrator', 'Owner'], + // }, + // }, + // ], + // }, + // { 'persondata.0._id': userId }, + // { 'persondata.0.role': 'Volunteer' }, + // { 'persondata.0.isVisible': true }, + // ], + // }, + // }, + // { + // $project: { + // personId: 1, + // name: 1, + // role: { + // $arrayElemAt: ['$persondata.role', 0], + // }, + // isVisible: { + // $arrayElemAt: ['$persondata.isVisible', 0], + // }, + // hasSummary: { + // $ne: [ + // { + // $arrayElemAt: [ + // { + // $arrayElemAt: ['$persondata.weeklySummaries.summary', 0], + // }, + // 0, + // ], + // }, + // '', + // ], + // }, + // weeklycommittedHours: { + // $sum: [ + // { + // $arrayElemAt: ['$persondata.weeklycommittedHours', 0], + // }, + // { + // $ifNull: [{ $arrayElemAt: ['$persondata.missedHours', 0] }, 0], + // }, + // ], + // }, + // }, + // }, + // { + // $lookup: { + // from: 'timeEntries', + // localField: 'personId', + // foreignField: 'personId', + // as: 'timeEntryData', + // }, + // }, + // { + // $project: { + // personId: 1, + // name: 1, + // role: 1, + // isVisible: 1, + // hasSummary: 1, + // weeklycommittedHours: 1, + // timeEntryData: { + // $filter: { + // input: '$timeEntryData', + // as: 'timeentry', + // cond: { + // $and: [ + // { + // $gte: ['$$timeentry.dateOfWork', pdtstart], + // }, + // { + // $lte: ['$$timeentry.dateOfWork', pdtend], + // }, + // ], + // }, + // }, + // }, + // }, + // }, + // { + // $unwind: { + // path: '$timeEntryData', + // preserveNullAndEmptyArrays: true, + // }, + // }, + // { + // $project: { + // personId: 1, + // name: 1, + // role: 1, + // isVisible: 1, + // hasSummary: 1, + // weeklycommittedHours: 1, + // totalSeconds: { + // $cond: [ + // { + // $gte: ['$timeEntryData.totalSeconds', 0], + // }, + // '$timeEntryData.totalSeconds', + // 0, + // ], + // }, + // isTangible: { + // $cond: [ + // { + // $gte: ['$timeEntryData.totalSeconds', 0], + // }, + // '$timeEntryData.isTangible', + // false, + // ], + // }, + // }, + // }, + // { + // $addFields: { + // tangibletime: { + // $cond: [ + // { + // $eq: ['$isTangible', true], + // }, + // '$totalSeconds', + // 0, + // ], + // }, + // intangibletime: { + // $cond: [ + // { + // $eq: ['$isTangible', false], + // }, + // '$totalSeconds', + // 0, + // ], + // }, + // }, + // }, + // { + // $group: { + // _id: { + // personId: '$personId', + // weeklycommittedHours: '$weeklycommittedHours', + // name: '$name', + // role: '$role', + // isVisible: '$isVisible', + // hasSummary: '$hasSummary', + // }, + // totalSeconds: { + // $sum: '$totalSeconds', + // }, + // tangibletime: { + // $sum: '$tangibletime', + // }, + // intangibletime: { + // $sum: '$intangibletime', + // }, + // }, + // }, + // { + // $project: { + // _id: 0, + // personId: '$_id.personId', + // name: '$_id.name', + // role: '$_id.role', + // isVisible: '$_id.isVisible', + // hasSummary: '$_id.hasSummary', + // weeklycommittedHours: '$_id.weeklycommittedHours', + // totaltime_hrs: { + // $divide: ['$totalSeconds', 3600], + // }, + // totaltangibletime_hrs: { + // $divide: ['$tangibletime', 3600], + // }, + // totalintangibletime_hrs: { + // $divide: ['$intangibletime', 3600], + // }, + // percentagespentintangible: { + // $cond: [ + // { + // $eq: ['$totalSeconds', 0], + // }, + // 0, + // { + // $multiply: [ + // { + // $divide: ['$tangibletime', '$totalSeconds'], + // }, + // 100, + // ], + // }, + // ], + // }, + // }, + // }, + // { + // $sort: { + // totaltangibletime_hrs: -1, + // name: 1, + // role: 1, + // }, + // }, + // ]); }; /** @@ -420,14 +586,14 @@ const dashboardhelper = function () { const getUserLaborData = async function (userId) { try { const pdtStart = moment() - .tz('America/Los_Angeles') - .startOf('week') - .format('YYYY-MM-DD'); + .tz("America/Los_Angeles") + .startOf("week") + .format("YYYY-MM-DD"); const pdtEnd = moment() - .tz('America/Los_Angeles') - .endOf('week') - .format('YYYY-MM-DD'); + .tz("America/Los_Angeles") + .endOf("week") + .format("YYYY-MM-DD"); const user = await userProfile.findById({ _id: userId, @@ -438,6 +604,7 @@ const dashboardhelper = function () { $gte: pdtStart, $lte: pdtEnd, }, + entryType: { $in: ["default", null] }, personId: userId, }); @@ -457,7 +624,7 @@ const dashboardhelper = function () { personId: userId, role: user.role, isVisible: user.isVisible, - hasSummary: user.weeklySummaries[0].summary !== '', + hasSummary: user.weeklySummaries[0].summary !== "", weeklycommittedHours: user.weeklycommittedHours, name: `${user.firstName} ${user.lastName}`, totaltime_hrs: (tangibleSeconds + intangibleSeconds) / 3600, @@ -465,13 +632,15 @@ const dashboardhelper = function () { totalintangibletime_hrs: intangibleSeconds / 3600, percentagespentintangible: (intangibleSeconds / tangibleSeconds) * 100, + timeOffFrom: user.timeOffFrom, + timeOffTill: user.timeOffTill, }, ]; } catch (err) { return [ { - personId: 'error', - name: 'Error Error', + personId: "error", + name: "Error Error", totaltime_hrs: 0, totaltangibletime_hrs: 0, totalintangibletime_hrs: 0, @@ -482,8 +651,8 @@ const dashboardhelper = function () { }; const laborthismonth = function (userId, startDate, endDate) { - const fromdate = moment(startDate).format('YYYY-MM-DD'); - const todate = moment(endDate).format('YYYY-MM-DD'); + const fromdate = moment(startDate).format("YYYY-MM-DD"); + const todate = moment(endDate).format("YYYY-MM-DD"); return timeentry.aggregate([ { @@ -499,19 +668,19 @@ const dashboardhelper = function () { { $group: { _id: { - projectId: '$projectId', + projectId: "$projectId", }, labor: { - $sum: '$totalSeconds', + $sum: "$totalSeconds", }, }, }, { $lookup: { - from: 'projects', - localField: '_id.projectId', - foreignField: '_id', - as: 'project', + from: "projects", + localField: "_id.projectId", + foreignField: "_id", + as: "project", }, }, { @@ -520,13 +689,13 @@ const dashboardhelper = function () { projectName: { $ifNull: [ { - $arrayElemAt: ['$project.projectName', 0], + $arrayElemAt: ["$project.projectName", 0], }, - 'Undefined', + "Undefined", ], }, timeSpent_hrs: { - $divide: ['$labor', 3600], + $divide: ["$labor", 3600], }, }, }, @@ -534,8 +703,8 @@ const dashboardhelper = function () { }; const laborthisweek = function (userId, startDate, endDate) { - const fromdate = moment(startDate).format('YYYY-MM-DD'); - const todate = moment(endDate).format('YYYY-MM-DD'); + const fromdate = moment(startDate).format("YYYY-MM-DD"); + const todate = moment(endDate).format("YYYY-MM-DD"); return userProfile.aggregate([ { @@ -551,10 +720,10 @@ const dashboardhelper = function () { }, { $lookup: { - from: 'timeEntries', - localField: '_id', - foreignField: 'personId', - as: 'timeEntryData', + from: "timeEntries", + localField: "_id", + foreignField: "personId", + as: "timeEntryData", }, }, { @@ -562,18 +731,28 @@ const dashboardhelper = function () { weeklycommittedHours: 1, timeEntryData: { $filter: { - input: '$timeEntryData', - as: 'timeentry', + input: "$timeEntryData", + as: "timeentry", cond: { $and: [ { - $eq: ['$$timeentry.isTangible', true], + $eq: ["$$timeentry.isTangible", true], + }, + { + $gte: ["$$timeentry.dateOfWork", fromdate], }, { - $gte: ['$$timeentry.dateOfWork', fromdate], + $lte: ["$$timeentry.dateOfWork", todate], }, { - $lte: ['$$timeentry.dateOfWork', todate], + $not: [ + { + $in: [ + "$$timeentry.entryType", + ["person", "team", "project"], + ], + }, + ], }, ], }, @@ -583,27 +762,27 @@ const dashboardhelper = function () { }, { $unwind: { - path: '$timeEntryData', + path: "$timeEntryData", preserveNullAndEmptyArrays: true, }, }, { $group: { _id: { - _id: '$_id', - weeklycommittedHours: '$weeklycommittedHours', + _id: "$_id", + weeklycommittedHours: "$weeklycommittedHours", }, effort: { - $sum: '$timeEntryData.totalSeconds', + $sum: "$timeEntryData.totalSeconds", }, }, }, { $project: { _id: 0, - weeklycommittedHours: '$_id.weeklycommittedHours', + weeklycommittedHours: "$_id.weeklycommittedHours", timeSpent_hrs: { - $divide: ['$effort', 3600], + $divide: ["$effort", 3600], }, }, }, @@ -611,8 +790,8 @@ const dashboardhelper = function () { }; const laborThisWeekByCategory = function (userId, startDate, endDate) { - const fromdate = moment(startDate).format('YYYY-MM-DD'); - const todate = moment(endDate).format('YYYY-MM-DD'); + const fromdate = moment(startDate).format("YYYY-MM-DD"); + const todate = moment(endDate).format("YYYY-MM-DD"); return userProfile.aggregate([ { @@ -628,10 +807,10 @@ const dashboardhelper = function () { }, { $lookup: { - from: 'timeEntries', - localField: '_id', - foreignField: 'personId', - as: 'timeEntryData', + from: "timeEntries", + localField: "_id", + foreignField: "personId", + as: "timeEntryData", }, }, { @@ -639,18 +818,28 @@ const dashboardhelper = function () { weeklycommittedHours: 1, timeEntryData: { $filter: { - input: '$timeEntryData', - as: 'timeentry', + input: "$timeEntryData", + as: "timeentry", cond: { $and: [ { - $eq: ['$$timeentry.isTangible', true], + $eq: ["$$timeentry.isTangible", true], + }, + { + $gte: ["$$timeentry.dateOfWork", fromdate], }, { - $gte: ['$$timeentry.dateOfWork', fromdate], + $lte: ["$$timeentry.dateOfWork", todate], }, { - $lte: ['$$timeentry.dateOfWork', todate], + $not: [ + { + $in: [ + "$$timeentry.entryType", + ["person", "team", "project"], + ], + }, + ], }, ], }, @@ -660,37 +849,37 @@ const dashboardhelper = function () { }, { $unwind: { - path: '$timeEntryData', + path: "$timeEntryData", preserveNullAndEmptyArrays: true, }, }, { $group: { - _id: '$timeEntryData.projectId', + _id: "$timeEntryData.projectId", effort: { - $sum: '$timeEntryData.totalSeconds', + $sum: "$timeEntryData.totalSeconds", }, }, }, { $lookup: { - from: 'projects', - localField: '_id', - foreignField: '_id', - as: 'project', + from: "projects", + localField: "_id", + foreignField: "_id", + as: "project", }, }, { $unwind: { - path: '$project', + path: "$project", preserveNullAndEmptyArrays: true, }, }, { $group: { - _id: '$project.category', + _id: "$project.category", effort: { - $sum: '$effort', + $sum: "$effort", }, }, }, @@ -698,7 +887,7 @@ const dashboardhelper = function () { $project: { _id: 1, timeSpent_hrs: { - $divide: ['$effort', 3600], + $divide: ["$effort", 3600], }, }, }, diff --git a/src/helpers/helperModels/userProjects.js b/src/helpers/helperModels/userProjects.js index 108ec345b..e325a0e39 100644 --- a/src/helpers/helperModels/userProjects.js +++ b/src/helpers/helperModels/userProjects.js @@ -5,6 +5,7 @@ const { Schema } = mongoose; const ProjectSchema = new Schema({ projectId: { type: mongoose.SchemaTypes.ObjectId, ref: 'projects' }, projectName: { type: String }, + category: { type: String }, }); diff --git a/src/helpers/reporthelper.js b/src/helpers/reporthelper.js index 0c2a8104d..eac0a71a1 100644 --- a/src/helpers/reporthelper.js +++ b/src/helpers/reporthelper.js @@ -1,5 +1,5 @@ -const moment = require("moment-timezone"); -const userProfile = require("../models/userProfile"); +const moment = require('moment-timezone'); +const userProfile = require('../models/userProfile'); /** * @@ -8,9 +8,9 @@ const userProfile = require("../models/userProfile"); * @returns The absolute value of the difference in weeks between the two input dates. */ const absoluteDifferenceInWeeks = (dateOfWork, pstEnd) => { - dateOfWork = moment(dateOfWork).endOf("week"); - pstEnd = moment(pstEnd).tz("America/Los_Angeles").endOf("week"); - return Math.abs(dateOfWork.diff(pstEnd, "weeks")); + dateOfWork = moment(dateOfWork).endOf('week'); + pstEnd = moment(pstEnd).tz('America/Los_Angeles').endOf('week'); + return Math.abs(dateOfWork.diff(pstEnd, 'weeks')); }; const reporthelper = function () { @@ -23,14 +23,14 @@ const reporthelper = function () { */ const weeklySummaries = async (startWeekIndex, endWeekIndex) => { const pstStart = moment() - .tz("America/Los_Angeles") - .startOf("week") - .subtract(startWeekIndex, "week") + .tz('America/Los_Angeles') + .startOf('week') + .subtract(startWeekIndex, 'week') .toDate(); const pstEnd = moment() - .tz("America/Los_Angeles") - .endOf("week") - .subtract(endWeekIndex, "week") + .tz('America/Los_Angeles') + .endOf('week') + .subtract(endWeekIndex, 'week') .toDate(); const results = await userProfile.aggregate([ @@ -39,33 +39,33 @@ const reporthelper = function () { }, { $lookup: { - from: "timeEntries", - localField: "_id", - foreignField: "personId", - as: "timeEntries", + from: 'timeEntries', + localField: '_id', + foreignField: 'personId', + as: 'timeEntries', }, }, { - $set: { totalTangibleHrs: { $objectToArray: "$hoursByCategory" } }, + $set: { totalTangibleHrs: { $objectToArray: '$hoursByCategory' } }, }, { $project: { timeEntries: { $filter: { - input: "$timeEntries", - as: "timeEntry", + input: '$timeEntries', + as: 'timeEntry', cond: { $and: [ { $gte: [ - "$$timeEntry.dateOfWork", - moment(pstStart).format("YYYY-MM-DD"), + '$$timeEntry.dateOfWork', + moment(pstStart).format('YYYY-MM-DD'), ], }, { $lte: [ - "$$timeEntry.dateOfWork", - moment(pstEnd).format("YYYY-MM-DD"), + '$$timeEntry.dateOfWork', + moment(pstEnd).format('YYYY-MM-DD'), ], }, ], @@ -86,22 +86,22 @@ const reporthelper = function () { bioPosted: 1, badgeCollection: { $filter: { - input: "$badgeCollection", - as: "badge", + input: '$badgeCollection', + as: 'badge', cond: { $or: [ { $and: [ { $gte: [ - "$$badge.earnedDate", - moment(pstStart).format("YYYY-MM-DD"), + '$$badge.earnedDate', + moment(pstStart).format('YYYY-MM-DD'), ], }, { $lte: [ - "$$badge.earnedDate", - moment(pstEnd).format("YYYY-MM-DD"), + '$$badge.earnedDate', + moment(pstEnd).format('YYYY-MM-DD'), ], }, ], @@ -109,10 +109,10 @@ const reporthelper = function () { { $and: [ { - $gte: ["$$badge.lastModified", pstStart], + $gte: ['$$badge.lastModified', pstStart], }, { - $lte: ["$$badge.lastModified", pstEnd], + $lte: ['$$badge.lastModified', pstEnd], }, ], }, @@ -121,20 +121,26 @@ const reporthelper = function () { }, }, teamCode: { - $ifNull: ['$teamCode', ''], + $ifNull: ["$teamCode", ""], + }, + timeOffFrom: { + $ifNull: ["$timeOffFrom", null], + }, + timeOffTill: { + $ifNull: ["$timeOffTill", null], }, role: 1, weeklySummaries: { $filter: { - input: "$weeklySummaries", - as: "ws", + input: '$weeklySummaries', + as: 'ws', cond: { $and: [ { - $gte: ["$$ws.dueDate", pstStart], + $gte: ['$$ws.dueDate', pstStart], }, { - $lte: ["$$ws.dueDate", pstEnd], + $lte: ['$$ws.dueDate', pstEnd], }, ], }, @@ -142,13 +148,13 @@ const reporthelper = function () { }, weeklySummariesCount: 1, isTangible: 1, - totalTangibleHrs: { $sum: "$totalTangibleHrs.v" }, + totalTangibleHrs: { $sum: '$totalTangibleHrs.v' }, daysInTeam: { $dateDiff: { - startDate: "$createdDate", + startDate: '$createdDate', endDate: new Date(), - unit: "day", - timezone: "America/Los_Angeles", + unit: 'day', + timezone: 'America/Los_Angeles', }, }, }, @@ -162,8 +168,8 @@ const reporthelper = function () { result.timeEntries.forEach((entry) => { const index = absoluteDifferenceInWeeks(entry.dateOfWork, pstEnd); if ( - result.totalSeconds[index] === undefined || - result.totalSeconds[index] === null + result.totalSeconds[index] === undefined + || result.totalSeconds[index] === null ) { result.totalSeconds[index] = 0; } @@ -189,16 +195,16 @@ const reporthelper = function () { */ const doesDateBelongToWeek = function (dueDate, weekIndex) { const pstStartOfWeek = moment() - .tz("America/Los_Angeles") - .startOf("week") - .subtract(weekIndex, "week"); + .tz('America/Los_Angeles') + .startOf('week') + .subtract(weekIndex, 'week'); const pstEndOfWeek = moment() - .tz("America/Los_Angeles") - .endOf("week") - .subtract(weekIndex, "week"); + .tz('America/Los_Angeles') + .endOf('week') + .subtract(weekIndex, 'week'); const fromDate = moment(pstStartOfWeek).toDate(); const toDate = moment(pstEndOfWeek).toDate(); - return moment(dueDate).isBetween(fromDate, toDate, undefined, "[]"); + return moment(dueDate).isBetween(fromDate, toDate, undefined, '[]'); }; /** diff --git a/src/helpers/taskHelper.js b/src/helpers/taskHelper.js index a94aaee94..372c960dd 100644 --- a/src/helpers/taskHelper.js +++ b/src/helpers/taskHelper.js @@ -1,309 +1,515 @@ -const moment = require('moment-timezone'); -const userProfile = require('../models/userProfile'); -const myteam = require('../helpers/helperModels/myTeam'); +const moment = require("moment-timezone"); +const mongoose = require("mongoose"); +const userProfile = require("../models/userProfile"); +const timeentry = require("../models/timeentry"); +const myTeam = require("../helpers/helperModels/myTeam"); +const team = require("../models/team"); +const Task = require("../models/task"); +const TaskNotification = require("../models/taskNotification"); +const Wbs = require("../models/wbs"); const taskHelper = function () { - const getTasksForTeams = function (userId) { - const pdtstart = moment() - .tz('America/Los_Angeles') - .startOf('week') - .format('YYYY-MM-DD'); - const pdtend = moment() - .tz('America/Los_Angeles') - .endOf('week') - .format('YYYY-MM-DD'); - return myteam.aggregate([ - { - $match: { - _id: userId, - }, - }, - { - $unwind: '$myteam', - }, - { - $project: { - _id: 0, - personId: '$myteam._id', - name: '$myteam.fullName', + const getTasksForTeams = async function (userId) { + const userid = mongoose.Types.ObjectId(userId); + const userById = await userProfile + .findOne( + { _id: userid, isActive: true }, + { role: 1, - }, - }, - // have personId, name, role - { - $lookup: { - from: 'userProfiles', - localField: 'personId', - foreignField: '_id', - as: 'persondata', - }, - }, - { - $match: { - // dashboard tasks user roles hierarchy - $or: [ - { - role: { $in: ['Owner', 'Core Team'] }, - }, - { - $and: [ - { - role: 'Administrator', - }, - { 'persondata.0.role': { $nin: ['Owner', 'Administrator'] } }, - ] - }, - { - $and: [ - { - role: { $in: ['Manager', 'Mentor'] }, - }, - { - 'persondata.0.role': { - $nin: ['Manager', 'Mentor', 'Core Team', 'Administrator', 'Owner'], - }, - }, - ], - }, - { 'persondata.0._id': userId }, - { 'persondata.0.role': 'Volunteer' }, - { 'persondata.0.isVisible': true }, - ], - }, - }, - { - $project: { - personId: 1, - name: 1, - weeklycommittedHours: { - $sum: [ - { - $arrayElemAt: ['$persondata.weeklycommittedHours', 0], - }, - { - $ifNull: [{ $arrayElemAt: ['$persondata.missedHours', 0] }, 0], - }, - ], - }, - role: 1, - }, - }, - { - $lookup: { - from: 'timeEntries', - localField: 'personId', - foreignField: 'personId', - as: 'timeEntryData', - }, - }, - { - $project: { - personId: 1, - name: 1, - weeklycommittedHours: 1, - timeEntryData: { - $filter: { - input: '$timeEntryData', - as: 'timeentry', - cond: { - $and: [ - { - $gte: ['$$timeentry.dateOfWork', pdtstart], - }, - { - $lte: ['$$timeentry.dateOfWork', pdtend], - }, - ], - }, - }, - }, + firstName: 1, + lastName: 1, role: 1, - }, - }, - { - $unwind: { - path: '$timeEntryData', - preserveNullAndEmptyArrays: true, - }, - }, - { - $project: { - personId: 1, - name: 1, + isVisible: 1, weeklycommittedHours: 1, - totalSeconds: { - $cond: [ - { - $gte: ['$timeEntryData.totalSeconds', 0], - }, - '$timeEntryData.totalSeconds', - 0, - ], - }, - isTangible: { - $cond: [ - { - $gte: ['$timeEntryData.totalSeconds', 0], - }, - '$timeEntryData.isTangible', - false, - ], - }, - role: 1, - }, - }, - { - $addFields: { - tangibletime: { - $cond: [ - { - $eq: ['$isTangible', true], - }, - '$totalSeconds', - 0, - ], - }, - }, - }, - { - $group: { - _id: { - personId: '$personId', - weeklycommittedHours: '$weeklycommittedHours', - name: '$name', - role: '$role', - }, - totalSeconds: { - $sum: '$totalSeconds', - }, - tangibletime: { - $sum: '$tangibletime', - }, - }, - }, - { - $project: { - _id: 0, - personId: '$_id.personId', - name: '$_id.name', - weeklycommittedHours: '$_id.weeklycommittedHours', - totaltime_hrs: { - $divide: ['$totalSeconds', 3600], - }, - totaltangibletime_hrs: { - $divide: ['$tangibletime', 3600], - }, - role: '$_id.role', - }, - }, - { - $lookup: { - from: 'tasks', - localField: 'personId', - foreignField: 'resources.userID', - as: 'tasks', - }, - }, - { - $project: { - tasks: { - resources: { - profilePic: 0, - }, - }, - }, - }, - { - $unwind: { - path: '$tasks', - preserveNullAndEmptyArrays: true, - }, - }, - { - $lookup: { - from: 'wbs', - localField: 'tasks.wbsId', - foreignField: '_id', - as: 'projectId', - }, - }, - { - $addFields: { - 'tasks.projectId': { - $cond: [ - { $ne: ['$projectId', []] }, - { $arrayElemAt: ['$projectId', 0] }, - '$tasks.projectId', - ], - }, - }, - }, - { - $project: { - projectId: 0, - tasks: { - projectId: { - _id: 0, - isActive: 0, - modifiedDatetime: 0, - wbsName: 0, - createdDatetime: 0, - __v: 0, - }, - }, - }, - }, - { - $addFields: { - 'tasks.projectId': '$tasks.projectId.projectId', - }, - }, - { - $lookup: { - from: 'taskNotifications', - localField: 'tasks._id', - foreignField: 'taskId', - as: 'tasks.taskNotifications', - }, - }, - { - $group: { - _id: '$personId', - tasks: { - $push: '$tasks', - }, - data: { - $first: '$$ROOT', - }, - }, - }, - { - $addFields: { - 'data.tasks': { - $filter: { - input: '$tasks', - as: 'task', - cond: { $ne: ['$$task', {}] }, - }, - }, - }, - }, - { - $replaceRoot: { - newRoot: '$data', - }, + weeklySummaries: 1, + timeOffFrom: 1, + timeOffTill: 1, + } + ) + .then((res) => res) + .catch((e) => {}); + + if (userById == null) return null; + const userRole = userById.role; + + const pdtstart = moment() + .tz("America/Los_Angeles") + .startOf("week") + .format("YYYY-MM-DD"); + const pdtend = moment() + .tz("America/Los_Angeles") + .endOf("week") + .format("YYYY-MM-DD"); + + let teamMemberIds = [userid]; + let teamMembers = []; + + if ( + userRole != "Administrator" && + userRole != "Owner" && + userRole != "Core Team" + ) { + // Manager , Mentor , Volunteer ... , Show only team members + const teamsResult = await team + .find({ "members.userId": { $in: [userid] } }, { members: 1 }) + .then((res) => res) + .catch((e) => {}); + + teamsResult.map((_myTeam) => { + _myTeam.members.map((teamMember) => { + if (!teamMember.userId.equals(userid)) + teamMemberIds.push(teamMember.userId); + }); + }); + teamsResult.map((_myTeam) => { + _myTeam.members.map((teamMember) => { + if (!teamMember.userId.equals(userid)) + teamMemberIds.push(teamMember.userId); + }); + }); + + teamMembers = await userProfile + .find( + { _id: { $in: teamMemberIds }, isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + timeOffFrom: 1, + timeOffTill: 1, + } + ) + .then((res) => res) + .catch((e) => {}); + } else if (userRole == "Administrator") { + // All users except Owner and Core Team + const excludedRoles = ["Core Team", "Owner"]; + teamMembers = await userProfile + .find( + { isActive: true, role: { $nin: excludedRoles } }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + timeOffFrom: 1, + timeOffTill: 1, + } + ) + .then((res) => res) + .catch((e) => {}); + } else { + // 'Core Team', 'Owner' //All users + teamMembers = await userProfile + .find( + { isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + timeOffFrom: 1, + timeOffTill: 1, + } + ) + .then((res) => res) + .catch((e) => {}); + } + + teamMemberIds = teamMembers.map((member) => member._id); + + const timeEntries = await timeentry.find({ + dateOfWork: { + $gte: pdtstart, + $lte: pdtend, }, - ]); + personId: { $in: teamMemberIds }, + }); + + const timeEntryByPerson = {}; + timeEntries.map((timeEntry) => { + const personIdStr = timeEntry.personId.toString(); + + if (timeEntryByPerson[personIdStr] == null) { + timeEntryByPerson[personIdStr] = { + tangibleSeconds: 0, + intangibleSeconds: 0, + totalSeconds: 0, + }; + } + + if (timeEntry.isTangible === true) { + timeEntryByPerson[personIdStr].tangibleSeconds += + timeEntry.totalSeconds; + } + timeEntryByPerson[personIdStr].totalSeconds += timeEntry.totalSeconds; + }); + + const teamMemberTasks = await Task.find( + { "resources.userID": { $in: teamMemberIds } }, + { "resources.profilePic": 0 } + ).populate({ + path: "wbsId", + select: "projectId", + }); + const teamMemberTaskIds = teamMemberTasks.map((task) => task._id); + const teamMemberTaskNotifications = await TaskNotification.find({ + taskId: { $in: teamMemberTaskIds }, + }); + + const taskNotificationByTaskNdUser = []; + teamMemberTaskNotifications.map((teamMemberTaskNotification) => { + const taskIdStr = teamMemberTaskNotification.taskId.toString(); + const userIdStr = teamMemberTaskNotification.userId.toString(); + const taskNdUserID = `${taskIdStr},${userIdStr}`; + + if (taskNotificationByTaskNdUser[taskNdUserID]) { + taskNotificationByTaskNdUser[taskNdUserID].push( + teamMemberTaskNotification + ); + } else { + taskNotificationByTaskNdUser[taskNdUserID] = [ + teamMemberTaskNotification, + ]; + } + }); + + const taskByPerson = []; + + teamMemberTasks.map((teamMemberTask) => { + const projId = teamMemberTask.wbsId?.projectId; + const _teamMemberTask = { ...teamMemberTask._doc }; + _teamMemberTask.projectId = projId; + const taskIdStr = _teamMemberTask._id.toString(); + + teamMemberTask.resources.map((resource) => { + const resourceIdStr = resource.userID.toString(); + const taskNdUserID = `${taskIdStr},${resourceIdStr}`; + _teamMemberTask.taskNotifications = + taskNotificationByTaskNdUser[taskNdUserID] || []; + if (taskByPerson[resourceIdStr]) { + taskByPerson[resourceIdStr].push(_teamMemberTask); + } else { + taskByPerson[resourceIdStr] = [_teamMemberTask]; + } + }); + }); + + const teamMemberTasksData = []; + teamMembers.map((teamMember) => { + const obj = { + personId: teamMember._id, + role: teamMember.role, + name: `${teamMember.firstName} ${teamMember.lastName}`, + weeklycommittedHours: teamMember.weeklycommittedHours, + totaltangibletime_hrs: + timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds / + 3600 || 0, + totaltime_hrs: + timeEntryByPerson[teamMember._id.toString()]?.totalSeconds / 3600 || + 0, + tasks: taskByPerson[teamMember._id.toString()] || [], + timeOffFrom: teamMember.timeOffFrom || null, + timeOffTill: teamMember.timeOffTill || null, + }; + teamMemberTasksData.push(obj); + }); + + return teamMemberTasksData; + + // return myteam.aggregate([ + // { + // $match: { + // _id: userId, + // }, + // }, + // { + // $unwind: '$myteam', + // }, + // { + // $project: { + // _id: 0, + // personId: '$myteam._id', + // name: '$myteam.fullName', + // role: 1, + // }, + // }, + // // have personId, name, role + // { + // $lookup: { + // from: 'userProfiles', + // localField: 'personId', + // foreignField: '_id', + // as: 'persondata', + // }, + // }, + // { + // $match: { + // // dashboard tasks user roles hierarchy + // $or: [ + // { + // role: { $in: ['Owner', 'Core Team'] }, + // }, + // { + // $and: [ + // { + // role: 'Administrator', + // }, + // { 'persondata.0.role': { $nin: ['Owner', 'Administrator'] } }, + // ], + // }, + // { + // $and: [ + // { + // role: { $in: ['Manager', 'Mentor'] }, + // }, + // { + // 'persondata.0.role': { + // $nin: ['Manager', 'Mentor', 'Core Team', 'Administrator', 'Owner'], + // }, + // }, + // ], + // }, + // { 'persondata.0._id': userId }, + // { 'persondata.0.role': 'Volunteer' }, + // { 'persondata.0.isVisible': true }, + // ], + // }, + // }, + // { + // $project: { + // personId: 1, + // name: 1, + // weeklycommittedHours: { + // $sum: [ + // { + // $arrayElemAt: ['$persondata.weeklycommittedHours', 0], + // }, + // { + // $ifNull: [{ $arrayElemAt: ['$persondata.missedHours', 0] }, 0], + // }, + // ], + // }, + // role: 1, + // }, + // }, + // { + // $lookup: { + // from: 'timeEntries', + // localField: 'personId', + // foreignField: 'personId', + // as: 'timeEntryData', + // }, + // }, + // { + // $project: { + // personId: 1, + // name: 1, + // weeklycommittedHours: 1, + // timeEntryData: { + // $filter: { + // input: '$timeEntryData', + // as: 'timeentry', + // cond: { + // $and: [ + // { + // $gte: ['$$timeentry.dateOfWork', pdtstart], + // }, + // { + // $lte: ['$$timeentry.dateOfWork', pdtend], + // }, + // ], + // }, + // }, + // }, + // role: 1, + // }, + // }, + // { + // $unwind: { + // path: '$timeEntryData', + // preserveNullAndEmptyArrays: true, + // }, + // }, + // { + // $project: { + // personId: 1, + // name: 1, + // weeklycommittedHours: 1, + // totalSeconds: { + // $cond: [ + // { + // $gte: ['$timeEntryData.totalSeconds', 0], + // }, + // '$timeEntryData.totalSeconds', + // 0, + // ], + // }, + // isTangible: { + // $cond: [ + // { + // $gte: ['$timeEntryData.totalSeconds', 0], + // }, + // '$timeEntryData.isTangible', + // false, + // ], + // }, + // role: 1, + // }, + // }, + // { + // $addFields: { + // tangibletime: { + // $cond: [ + // { + // $eq: ['$isTangible', true], + // }, + // '$totalSeconds', + // 0, + // ], + // }, + // }, + // }, + // { + // $group: { + // _id: { + // personId: '$personId', + // weeklycommittedHours: '$weeklycommittedHours', + // name: '$name', + // role: '$role', + // }, + // totalSeconds: { + // $sum: '$totalSeconds', + // }, + // tangibletime: { + // $sum: '$tangibletime', + // }, + // }, + // }, + // { + // $project: { + // _id: 0, + // personId: '$_id.personId', + // name: '$_id.name', + // weeklycommittedHours: '$_id.weeklycommittedHours', + // totaltime_hrs: { + // $divide: ['$totalSeconds', 3600], + // }, + // totaltangibletime_hrs: { + // $divide: ['$tangibletime', 3600], + // }, + // role: '$_id.role', + // }, + // }, + // { + // $lookup: { + // from: 'tasks', + // localField: 'personId', + // foreignField: 'resources.userID', + // as: 'tasks', + // }, + // }, + // { + // $project: { + // tasks: { + // resources: { + // profilePic: 0, + // }, + // }, + // }, + // }, + // { + // $unwind: { + // path: '$tasks', + // preserveNullAndEmptyArrays: true, + // }, + // }, + // { + // $lookup: { + // from: 'wbs', + // localField: 'tasks.wbsId', + // foreignField: '_id', + // as: 'projectId', + // }, + // }, + // { + // $addFields: { + // 'tasks.projectId': { + // $cond: [ + // { $ne: ['$projectId', []] }, + // { $arrayElemAt: ['$projectId', 0] }, + // '$tasks.projectId', + // ], + // }, + // }, + // }, + // { + // $project: { + // projectId: 0, + // tasks: { + // projectId: { + // _id: 0, + // isActive: 0, + // modifiedDatetime: 0, + // wbsName: 0, + // createdDatetime: 0, + // __v: 0, + // }, + // }, + // }, + // }, + // { + // $addFields: { + // 'tasks.projectId': '$tasks.projectId.projectId', + // }, + // }, + // { + // $lookup: { + // from: 'taskNotifications', + // localField: 'tasks._id', + // foreignField: 'taskId', + // as: 'tasks.taskNotifications', + // }, + // }, + // { + // $group: { + // _id: '$personId', + // tasks: { + // $push: '$tasks', + // }, + // data: { + // $first: '$$ROOT', + // }, + // }, + // }, + // { + // $addFields: { + // 'data.tasks': { + // $filter: { + // input: '$tasks', + // as: 'task', + // cond: { $ne: ['$$task', {}] }, + // }, + // }, + // }, + // }, + // { + // $replaceRoot: { + // newRoot: '$data', + // }, + // }, + // ]); }; const getTasksForSingleUser = function (userId) { const pdtstart = moment() - .tz('America/Los_Angeles') - .startOf('week') - .format('YYYY-MM-DD'); + .tz("America/Los_Angeles") + .startOf("week") + .format("YYYY-MM-DD"); const pdtend = moment() - .tz('America/Los_Angeles') - .endOf('week') - .format('YYYY-MM-DD'); + .tz("America/Los_Angeles") + .endOf("week") + .format("YYYY-MM-DD"); return userProfile.aggregate([ { $match: { @@ -312,31 +518,33 @@ const taskHelper = function () { }, { $project: { - personId: '$_id', - role: '$role', + personId: "$_id", + role: "$role", name: { - $concat: [ - '$firstName', - ' ', - '$lastName', - ], + $concat: ["$firstName", " ", "$lastName"], }, weeklycommittedHours: { $sum: [ - '$weeklycommittedHours', + "$weeklycommittedHours", { - $ifNull: ['$missedHours', 0], + $ifNull: ["$missedHours", 0], }, ], }, + timeOffFrom: { + $ifNull: ["$timeOffFrom", null], + }, + timeOffTill: { + $ifNull: ["$timeOffTill", null], + }, }, }, { $lookup: { - from: 'timeEntries', - localField: 'personId', - foreignField: 'personId', - as: 'timeEntryData', + from: "timeEntries", + localField: "personId", + foreignField: "personId", + as: "timeEntryData", }, }, { @@ -344,18 +552,23 @@ const taskHelper = function () { personId: 1, name: 1, weeklycommittedHours: 1, + timeOffFrom: 1, + timeOffTill: 1, role: 1, timeEntryData: { $filter: { - input: '$timeEntryData', - as: 'timeentry', + input: "$timeEntryData", + as: "timeentry", cond: { $and: [ { - $gte: ['$$timeentry.dateOfWork', pdtstart], + $gte: ["$$timeentry.dateOfWork", pdtstart], + }, + { + $lte: ["$$timeentry.dateOfWork", pdtend], }, { - $lte: ['$$timeentry.dateOfWork', pdtend], + $in: ["$$timeentry.entryType", ["default", null]], }, ], }, @@ -365,7 +578,7 @@ const taskHelper = function () { }, { $unwind: { - path: '$timeEntryData', + path: "$timeEntryData", preserveNullAndEmptyArrays: true, }, }, @@ -374,22 +587,24 @@ const taskHelper = function () { personId: 1, name: 1, weeklycommittedHours: 1, + timeOffFrom: 1, + timeOffTill: 1, role: 1, totalSeconds: { $cond: [ { - $gte: ['$timeEntryData.totalSeconds', 0], + $gte: ["$timeEntryData.totalSeconds", 0], }, - '$timeEntryData.totalSeconds', + "$timeEntryData.totalSeconds", 0, ], }, isTangible: { $cond: [ { - $gte: ['$timeEntryData.totalSeconds', 0], + $gte: ["$timeEntryData.totalSeconds", 0], }, - '$timeEntryData.isTangible', + "$timeEntryData.isTangible", false, ], }, @@ -400,9 +615,9 @@ const taskHelper = function () { tangibletime: { $cond: [ { - $eq: ['$isTangible', true], + $eq: ["$isTangible", true], }, - '$totalSeconds', + "$totalSeconds", 0, ], }, @@ -411,40 +626,44 @@ const taskHelper = function () { { $group: { _id: { - personId: '$personId', - weeklycommittedHours: '$weeklycommittedHours', - name: '$name', - role: '$role', + personId: "$personId", + weeklycommittedHours: "$weeklycommittedHours", + timeOffFrom: "$timeOffFrom", + timeOffTill: "$timeOffTill", + name: "$name", + role: "$role", }, totalSeconds: { - $sum: '$totalSeconds', + $sum: "$totalSeconds", }, tangibletime: { - $sum: '$tangibletime', + $sum: "$tangibletime", }, }, }, { $project: { _id: 0, - personId: '$_id.personId', - name: '$_id.name', - weeklycommittedHours: '$_id.weeklycommittedHours', - role: '$_id.role', + personId: "$_id.personId", + name: "$_id.name", + weeklycommittedHours: "$_id.weeklycommittedHours", + timeOffFrom: "$_id.timeOffFrom", + timeOffTill: "$_id.timeOffTill", + role: "$_id.role", totaltime_hrs: { - $divide: ['$totalSeconds', 3600], + $divide: ["$totalSeconds", 3600], }, totaltangibletime_hrs: { - $divide: ['$tangibletime', 3600], + $divide: ["$tangibletime", 3600], }, }, }, { $lookup: { - from: 'tasks', - localField: 'personId', - foreignField: 'resources.userID', - as: 'tasks', + from: "tasks", + localField: "personId", + foreignField: "resources.userID", + as: "tasks", }, }, { @@ -458,25 +677,25 @@ const taskHelper = function () { }, { $unwind: { - path: '$tasks', + path: "$tasks", preserveNullAndEmptyArrays: true, }, }, { $lookup: { - from: 'wbs', - localField: 'tasks.wbsId', - foreignField: '_id', - as: 'projectId', + from: "wbs", + localField: "tasks.wbsId", + foreignField: "_id", + as: "projectId", }, }, { $addFields: { - 'tasks.projectId': { + "tasks.projectId": { $cond: [ - { $ne: ['$projectId', []] }, - { $arrayElemAt: ['$projectId', 0] }, - '$tasks.projectId', + { $ne: ["$projectId", []] }, + { $arrayElemAt: ["$projectId", 0] }, + "$tasks.projectId", ], }, }, @@ -498,40 +717,40 @@ const taskHelper = function () { }, { $addFields: { - 'tasks.projectId': '$tasks.projectId.projectId', + "tasks.projectId": "$tasks.projectId.projectId", }, }, { $lookup: { - from: 'taskNotifications', - localField: 'tasks._id', - foreignField: 'taskId', - as: 'tasks.taskNotifications', + from: "taskNotifications", + localField: "tasks._id", + foreignField: "taskId", + as: "tasks.taskNotifications", }, }, { $group: { - _id: '$personId', - tasks: { $push: '$tasks' }, + _id: "$personId", + tasks: { $push: "$tasks" }, data: { - $first: '$$ROOT', + $first: "$$ROOT", }, }, }, { $addFields: { - 'data.tasks': { + "data.tasks": { $filter: { - input: '$tasks', - as: 'task', - cond: { $ne: ['$$task', {}] }, + input: "$tasks", + as: "task", + cond: { $ne: ["$$task", {}] }, }, }, }, }, { $replaceRoot: { - newRoot: '$data', + newRoot: "$data", }, }, ]); @@ -539,7 +758,7 @@ const taskHelper = function () { const getUserProfileFirstAndLastName = function (userId) { return userProfile.findById(userId).then((results) => { if (!results) { - return ' '; + return " "; } return `${results.firstName} ${results.lastName}`; }); diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index af416f1a7..b4b2acb46 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -1,85 +1,74 @@ /* eslint-disable no-continue */ /* eslint-disable no-await-in-loop */ -const mongoose = require("mongoose"); -const moment = require("moment-timezone"); -const _ = require("lodash"); -const userProfile = require("../models/userProfile"); -const timeEntries = require("../models/timeentry"); -const badge = require("../models/badge"); -const myTeam = require("./helperModels/myTeam"); -const dashboardHelper = require("./dashboardhelper")(); -const reportHelper = require("./reporthelper")(); -const emailSender = require("../utilities/emailSender"); -const logger = require("../startup/logger"); -const hasPermission = require("../utilities/permissions"); -const Reason = require("../models/reason"); -const token = require("../models/profileInitialSetupToken") +const mongoose = require('mongoose'); +const moment = require('moment-timezone'); +const _ = require('lodash'); +const userProfile = require('../models/userProfile'); +const timeEntries = require('../models/timeentry'); +const badge = require('../models/badge'); +const myTeam = require('./helperModels/myTeam'); +const dashboardHelper = require('./dashboardhelper')(); +const reportHelper = require('./reporthelper')(); +const emailSender = require('../utilities/emailSender'); +const logger = require('../startup/logger'); +const Reason = require('../models/reason'); +const token = require('../models/profileInitialSetupToken'); +const cache = require('../utilities/nodeCache')(); const userHelper = function () { + // Update format to "MMM-DD-YY" from "YYYY-MMM-DD" (Confirmed with Jae) + const earnedDateBadge = () => { + const currentDate = new Date(Date.now()); + return moment(currentDate).tz('America/Los_Angeles').format('MMM-DD-YY'); + }; + + const getTeamMembers = function (user) { const userId = mongoose.Types.ObjectId(user._id); // var teamid = userdetails.teamId; return myTeam.findById(userId).select({ - "myTeam._id": 0, - "myTeam.role": 0, - "myTeam.fullName": 0, + 'myTeam._id': 0, + 'myTeam.role': 0, + 'myTeam.fullName': 0, _id: 0, }); }; - const earnedDateBadge = () => { - const today = new Date(); - const yyyy = today.getFullYear(); - // Add 1 beacuse the month start at zero - let mm = today.getMonth() + 1; - let dd = today.getDate(); - - // eslint-disable-next-line no-unused-expressions - mm = mm < 10 ? `0${mm}` : mm; - // eslint-disable-next-line no-unused-expressions - dd = dd < 10 ? `0${dd}` : dd; - - const formatedDate = `${yyyy}-${mm}-${dd}`; - - return formatedDate; - }; - const getUserName = async function (userId) { const userid = mongoose.Types.ObjectId(userId); - return userProfile.findById(userid, "firstName lastName"); + return userProfile.findById(userid, 'firstName lastName'); }; const validateProfilePic = function (profilePic) { - const picParts = profilePic.split("base64"); + const picParts = profilePic.split('base64'); let result = true; const errors = []; if (picParts.length < 2) { return { result: false, - errors: "Invalid image" + errors: 'Invalid image', }; } // validate size const imageSize = picParts[1].length; - const sizeInBytes = - (4 * Math.ceil(imageSize / 3) * 0.5624896334383812) / 1024; + const sizeInBytes = (4 * Math.ceil(imageSize / 3) * 0.5624896334383812) / 1024; if (sizeInBytes > 50) { - errors.push("Image size should not exceed 50KB"); + errors.push('Image size should not exceed 50KB'); result = false; } - const imageType = picParts[0].split("/")[1]; - if (imageType !== "jpeg;" && imageType !== "png;") { - errors.push("Image type shoud be either jpeg or png."); + const imageType = picParts[0].split('/')[1]; + if (imageType !== 'jpeg;' && imageType !== 'png;') { + errors.push('Image type shoud be either jpeg or png.'); result = false; } return { result, - errors + errors, }; }; @@ -87,18 +76,29 @@ const userHelper = function () { firstName, lastName, infringement, - totalInfringements + totalInfringements, + timeRemaining, ) { + let final_paragraph = ''; + + if (timeRemaining == undefined) { + final_paragraph = 'Life happens and we understand that. That’s why we allow 5 of them before taking action. This action usually includes removal from our team though, so please let your direct supervisor know what happened and do your best to avoid future blue squares if you are getting close to 5 and wish to avoid termination. Each blue square drops off after a year.
'; + } else { + final_paragraph = `Life happens and we understand that. Please make up the missed hours this following week though to avoid getting another blue square. So you know what’s needed, the missing/incomplete hours (${timeRemaining} hours) have been added to your current week and this new weekly total can be seen at the top of your dashboard.
+Reminder also that each blue square is removed from your profile 1 year after it was issued.
`; + } + const text = `Dear ${firstName} ${lastName},Oops, it looks like something happened and you’ve managed to get a blue square.
Date Assigned: ${infringement.date}
Description: ${infringement.description}
Total Infringements: This is your ${moment - .localeData() - .ordinal(totalInfringements)} blue square of 5.
-Life happens and we understand that. That’s why we allow 5 of them before taking action. This action usually includes removal from our team though, so please let your direct supervisor know what happened and do your best to avoid future blue squares if you are getting close to 5 and wish to avoid termination. Each blue square drops off after a year.
+ .localeData() + .ordinal(totalInfringements)} blue square of 5. + ${final_paragraph}Thank you,
One Community
- Media URL: ${mediaUrlLink || 'Not provided!'} + Media URL: ${ + mediaUrlLink || 'Not provided!' + }
- ${weeklySummariesCount === 8 - ? `Total Valid Weekly Summaries: ${weeklySummariesCount}
` - : `Total Valid Weekly Summaries: ${weeklySummariesCount || - "No valid submissions yet!"}
` + ${ + weeklySummariesCount === 8 + ? `Total Valid Weekly Summaries: ${weeklySummariesCount}
` + : `Total Valid Weekly Summaries: ${ + weeklySummariesCount || 'No valid submissions yet!' + }
` } - ${hoursLogged >= weeklycommittedHours - - ? `Hours logged: ${hoursLogged.toFixed(2)} / ${weeklycommittedHours}
` - - : `Hours logged: ${hoursLogged.toFixed( - 2 - )} / ${weeklycommittedHours}
` + ${ + hoursLogged >= weeklycommittedHours + ? `Hours logged: ${hoursLogged.toFixed( + 2, + )} / ${weeklycommittedHours}
` + : `Hours logged: ${hoursLogged.toFixed( + 2, + )} / ${weeklycommittedHours}
` } ${weeklySummaryMessage}The HGN A.I. (and One Community)
`; - - emailSender('onecommunityglobal@gmail.com', subject, emailBody, null, null); - + emailSender( + 'onecommunityglobal@gmail.com', + subject, + emailBody, + null, + null, + person.email, + ); } } } catch (err) { @@ -720,60 +740,74 @@ const userHelper = function () { } }; - - const notifyInfringements = function (original, current, firstName, lastName, emailAddress) { - + const notifyInfringements = function ( + original, + current, + firstName, + lastName, + emailAddress, + ) { if (!current) return; const newOriginal = original.toObject(); const newCurrent = current.toObject(); const totalInfringements = newCurrent.length; let newInfringements = []; - newInfringements = _.differenceWith(newCurrent, newOriginal, (arrVal, othVal) => arrVal._id.equals(othVal._id)); + newInfringements = _.differenceWith( + newCurrent, + newOriginal, + (arrVal, othVal) => arrVal._id.equals(othVal._id), + ); newInfringements.forEach((element) => { emailSender( emailAddress, 'New Infringement Assigned', - getInfringementEmailBody(firstName, lastName, element, totalInfringements), + getInfringementEmailBody( + firstName, + lastName, + element, + totalInfringements, + ), null, - "onecommunityglobal@gmail.com" + 'onecommunityglobal@gmail.com', + emailAddress, ); }); }; const replaceBadge = async function (personId, oldBadgeId, newBadgeId) { userProfile.updateOne( - { _id: personId, "badgeCollection.badge": oldBadgeId }, + { _id: personId, 'badgeCollection.badge': oldBadgeId }, { $set: { - "badgeCollection.$.badge": newBadgeId, - "badgeCollection.$.lastModified": Date.now().toString(), - "badgeCollection.$.count": 1 - } + 'badgeCollection.$.badge': newBadgeId, + 'badgeCollection.$.lastModified': Date.now().toString(), + 'badgeCollection.$.count': 1, + 'badgeCollection.$.earnedDate': [earnedDateBadge()], + }, }, - err => { + (err) => { if (err) { throw new Error(err); } - } + }, ); }; const increaseBadgeCount = async function (personId, badgeId) { - console.log("Increase Badge Count", personId, badgeId); userProfile.updateOne( - { _id: personId, "badgeCollection.badge": badgeId }, + { _id: personId, 'badgeCollection.badge': badgeId }, { - $inc: { "badgeCollection.$.count": 1 }, - $set: { "badgeCollection.$.lastModified": Date.now().toString() }, - $push: { "badgeCollection.$.earnedDate": earnedDateBadge() } + $inc: { 'badgeCollection.$.count': 1 }, + $set: { 'badgeCollection.$.lastModified': Date.now().toString() }, + $push: { 'badgeCollection.$.earnedDate': earnedDateBadge() }, }, - err => { + (err) => { if (err) { console.log(err); } - } + }, ); }; @@ -781,23 +815,26 @@ const userHelper = function () { personId, badgeId, count = 1, - featured = false + featured = false, ) { - console.log('Adding Badge'); userProfile.findByIdAndUpdate( personId, { $push: { badgeCollection: { - badge: badgeId, count, earnedDate: [earnedDateBadge()], featured, lastModified: Date.now().toString(), + badge: badgeId, + count, + earnedDate: [earnedDateBadge()], + featured, + lastModified: Date.now().toString(), }, }, }, - err => { + (err) => { if (err) { throw new Error(err); } - } + }, ); }; @@ -806,42 +843,73 @@ const userHelper = function () { personId, { $pull: { - badgeCollection: { badge: badgeId } - } + badgeCollection: { badge: badgeId }, + }, }, - err => { + (err) => { if (err) { throw new Error(err); } - } + }, ); }; - const changeBadgeCount = async function (personId, badgeId, count) { +const changeBadgeCount = async function (personId, badgeId, count) { if (count === 0) { removeDupBadge(personId, badgeId); } else if (count) { - userProfile.updateOne( - { _id: personId, "badgeCollection.badge": badgeId }, - { - $set: { - "badgeCollection.$.count": count, - "badgeCollection.$.lastModified": Date.now().toString() + // Process exisiting earned date to match the new count + try { + const userInfo = await userProfile.findById(personId); + let newEarnedDate = []; + const recordToUpdate = userInfo.badgeCollection.find(item => item.badge._id.toString() === badgeId.toString()); + if (!recordToUpdate) { + throw new Error('Badge not found'); + } + const copyOfEarnedDate = recordToUpdate.earnedDate; + if (copyOfEarnedDate.length < count) { + // if the EarnedDate count is less than the new count, add a earned date to the end of the collection + while (copyOfEarnedDate.length < count) { + copyOfEarnedDate.push(earnedDateBadge()); } - }, - err => { - if (err) { - throw new Error(err); + } else { + // if the EarnedDate count is greater than the new count, remove the oldest earned date of the collection until it matches the new count - 1 + while (copyOfEarnedDate.length >= count) { + copyOfEarnedDate.shift(); } + copyOfEarnedDate.push(earnedDateBadge()); } - ); + newEarnedDate = [...copyOfEarnedDate]; + userProfile.updateOne( + { _id: personId, 'badgeCollection.badge': badgeId }, + { + $set: { + 'badgeCollection.$.count': count, + 'badgeCollection.$.lastModified': Date.now().toString(), + 'badgeCollection.$.earnedDate': newEarnedDate, + }, + }, + (err) => { + if (err) { + throw new Error(err); + } + }, + ); + } catch (err) { + logger.logException(err); + } } }; // remove the last badge you earned on this streak(not including 1) - const removePrevHrBadge = async function (personId, user, badgeCollection, hrs, weeks) { - + const removePrevHrBadge = async function ( + personId, + user, + badgeCollection, + hrs, + weeks, + ) { // Check each Streak Greater than One to check if it works if (weeks < 3) { return; @@ -851,37 +919,36 @@ const userHelper = function () { .aggregate([ { $match: { - type: "X Hours for X Week Streak", + type: 'X Hours for X Week Streak', weeks: { $gt: 1, $lt: weeks }, - totalHrs: hrs - } + totalHrs: hrs, + }, }, { $sort: { weeks: -1, totalHrs: -1 } }, { $group: { - _id: "$weeks", + _id: '$weeks', badges: { - $push: { _id: "$_id", hrs: "$totalHrs", weeks: "$weeks" } - } - } - } + $push: { _id: '$_id', hrs: '$totalHrs', weeks: '$weeks' }, + }, + }, + }, ]) - .then(results => { - results.forEach(streak => { - streak.badges.every(bdge => { + .then((results) => { + results.forEach((streak) => { + streak.badges.every((bdge) => { for (let i = 0; i < badgeCollection.length; i += 1) { if ( - - badgeCollection[i].badge?.type === 'X Hours for X Week Streak' + badgeCollection[i].badge?.type + === 'X Hours for X Week Streak' && badgeCollection[i].badge?.weeks === bdge.weeks && bdge.hrs === hrs && !removed - ) { changeBadgeCount( personId, badgeCollection[i].badge._id, - badgeCollection[i].badge.count - 1 + badgeCollection[i].badge.count - 1, ); removed = true; return false; @@ -894,16 +961,24 @@ const userHelper = function () { }; // 'No Infringement Streak', - - const checkNoInfringementStreak = async function (personId, user, badgeCollection) { + const checkNoInfringementStreak = async function ( + personId, + user, + badgeCollection, + ) { let badgeOfType; for (let i = 0; i < badgeCollection.length; i += 1) { if (badgeCollection[i].badge?.type === 'No Infringement Streak') { - if (badgeOfType && badgeOfType.months <= badgeCollection[i].badge.months) { + if ( + badgeOfType + && badgeOfType.months <= badgeCollection[i].badge.months + ) { removeDupBadge(personId, badgeOfType._id); badgeOfType = badgeCollection[i].badge; - } else if (badgeOfType && badgeOfType.months > badgeCollection[i].badge.months) { - + } else if ( + badgeOfType + && badgeOfType.months > badgeCollection[i].badge.months + ) { removeDupBadge(personId, badgeCollection[i].badge._id); } else if (!badgeOfType) { badgeOfType = badgeCollection[i].badge; @@ -911,30 +986,31 @@ const userHelper = function () { } } await badge - .find({ type: "No Infringement Streak" }) + .find({ type: 'No Infringement Streak' }) .sort({ months: -1 }) - .then(results => { + .then((results) => { if (!Array.isArray(results) || !results.length) { return; } - results.every(elem => { + results.every((elem) => { // Cannot account for time paused yet if (elem.months <= 12) { - - if (moment().diff(moment(user.createdDate), 'months', true) >= elem.months) { - + if ( + moment().diff(moment(user.createdDate), 'months', true) + >= elem.months + ) { if ( - user.infringements.length === 0 || - Math.abs( + user.infringements.length === 0 + || Math.abs( moment().diff( - - moment(user.infringements[user.infringements?.length - 1].date), + moment( + user.infringements[user.infringements?.length - 1].date, + ), 'months', true, ), - ) >= elem.months ) { if (badgeOfType) { @@ -942,7 +1018,7 @@ const userHelper = function () { replaceBadge( personId, mongoose.Types.ObjectId(badgeOfType._id), - mongoose.Types.ObjectId(elem._id) + mongoose.Types.ObjectId(elem._id), ); } return false; @@ -952,28 +1028,30 @@ const userHelper = function () { } } } else if (user?.infringements?.length === 0) { - - if (moment().diff(moment(user.createdDate), 'months', true) >= elem.months) { - + if ( + moment().diff(moment(user.createdDate), 'months', true) + >= elem.months + ) { if ( - user.oldInfringements.length === 0 || - Math.abs( + user.oldInfringements.length === 0 + || Math.abs( moment().diff( - - moment(user.oldInfringements[user.oldInfringements?.length - 1].date), + moment( + user.oldInfringements[user.oldInfringements?.length - 1] + .date, + ), 'months', true, ), ) - >= elem.months - 12 - + >= elem.months - 12 ) { if (badgeOfType) { if (badgeOfType._id.toString() !== elem._id.toString()) { replaceBadge( personId, mongoose.Types.ObjectId(badgeOfType._id), - mongoose.Types.ObjectId(elem._id) + mongoose.Types.ObjectId(elem._id), ); } return false; @@ -992,11 +1070,11 @@ const userHelper = function () { const checkMinHoursMultiple = async function ( personId, user, - badgeCollection + badgeCollection, ) { const badgesOfType = badgeCollection .map(obj => obj.badge) - .filter(badge => badge.type === 'Minimum Hours Multiple') + .filter(badgeItem => badgeItem.type === 'Minimum Hours Multiple'); await badge .find({ type: 'Minimum Hours Multiple' }) .sort({ multiple: -1 }) @@ -1009,49 +1087,51 @@ const userHelper = function () { const elem = results[i]; // making variable elem accessible for below code if ( - user.lastWeekTangibleHrs / user.weeklycommittedHours >= - elem.multiple + user.lastWeekTangibleHrs / user.weeklycommittedHours + >= elem.multiple ) { const theBadge = badgesOfType.find( - (badge) => badge._id.toString() === elem._id.toString() + badgeItem => badgeItem._id.toString() === elem._id.toString(), ); return theBadge ? increaseBadgeCount( - personId, - mongoose.Types.ObjectId(theBadge._id) - ) + personId, + mongoose.Types.ObjectId(theBadge._id), + ) : addBadge(personId, mongoose.Types.ObjectId(elem._id)); } } - }) + }); }; // 'Personal Max', const checkPersonalMax = async function (personId, user, badgeCollection) { let badgeOfType; for (let i = 0; i < badgeCollection.length; i += 1) { - if (badgeCollection[i].badge?.type === "Personal Max") { + if (badgeCollection[i].badge?.type === 'Personal Max') { if (badgeOfType) { removeDupBadge(personId, badgeOfType._id); } } } - await badge.findOne({ type: "Personal Max" }).then(results => { + await badge.findOne({ type: 'Personal Max' }).then((results) => { if ( - user.lastWeekTangibleHrs && - user.lastWeekTangibleHrs >= 1 && - user.lastWeekTangibleHrs === user.personalBestMaxHrs + user.lastWeekTangibleHrs + && user.lastWeekTangibleHrs >= 1 + && user.lastWeekTangibleHrs === user.personalBestMaxHrs ) { if (badgeOfType) { changeBadgeCount( personId, mongoose.Types.ObjectId(badgeOfType._id), - user.personalBestMaxHrs + user.personalBestMaxHrs, ); } else { - - addBadge(personId, mongoose.Types.ObjectId(results._id), user.personalBestMaxHrs); - + addBadge( + personId, + mongoose.Types.ObjectId(results._id), + user.personalBestMaxHrs, + ); } } }); @@ -1071,22 +1151,21 @@ const userHelper = function () { userProfile .aggregate([ { $match: { isActive: true } }, - { $group: { _id: 1, maxHours: { $max: "$lastWeekTangibleHrs" } } }, + { $group: { _id: 1, maxHours: { $max: '$lastWeekTangibleHrs' } } }, ]) .then((userResults) => { if (badgeOfType.length > 1) { removeDupBadge(user._id, badgeOfType[0]._id); - } if ( - user.lastWeekTangibleHrs && - user.lastWeekTangibleHrs >= userResults[0].maxHours + user.lastWeekTangibleHrs + && user.lastWeekTangibleHrs >= userResults[0].maxHours ) { if (badgeOfType.length) { increaseBadgeCount( personId, - mongoose.Types.ObjectId(badgeOfType[0]._id) + mongoose.Types.ObjectId(badgeOfType[0]._id), ); } else { addBadge(personId, mongoose.Types.ObjectId(results._id)); @@ -1102,15 +1181,15 @@ const userHelper = function () { // Handle Increasing the 1 week streak badges const badgesOfType = []; for (let i = 0; i < badgeCollection.length; i += 1) { - if (badgeCollection[i].badge?.type === "X Hours for X Week Streak") { + if (badgeCollection[i].badge?.type === 'X Hours for X Week Streak') { badgesOfType.push(badgeCollection[i].badge); } } await badge - .find({ type: "X Hours for X Week Streak", weeks: 1 }) + .find({ type: 'X Hours for X Week Streak', weeks: 1 }) .sort({ totalHrs: -1 }) - .then(results => { - results.every(elem => { + .then((results) => { + results.every((elem) => { if (elem.totalHrs <= user.lastWeekTangibleHrs) { let theBadge; for (let i = 0; i < badgesOfType.length; i += 1) { @@ -1132,35 +1211,37 @@ const userHelper = function () { // Check each Streak Greater than One to check if it works await badge .aggregate([ - { $match: { type: "X Hours for X Week Streak", weeks: { $gt: 1 } } }, + { $match: { type: 'X Hours for X Week Streak', weeks: { $gt: 1 } } }, { $sort: { weeks: -1, totalHrs: -1 } }, { $group: { - _id: "$weeks", + _id: '$weeks', badges: { - $push: { _id: "$_id", hrs: "$totalHrs", weeks: "$weeks" } - } - } - } + $push: { _id: '$_id', hrs: '$totalHrs', weeks: '$weeks' }, + }, + }, + }, ]) - .then(results => { + .then((results) => { let lastHr = -1; - results.forEach(streak => { - streak.badges.every(bdge => { + results.forEach((streak) => { + streak.badges.every((bdge) => { let badgeOfType; for (let i = 0; i < badgeCollection.length; i += 1) { if ( - - badgeCollection[i].badge?.type === 'X Hours for X Week Streak' + badgeCollection[i].badge?.type + === 'X Hours for X Week Streak' && badgeCollection[i].badge?.weeks === bdge.weeks ) { - if (badgeOfType && badgeOfType.totalHrs <= badgeCollection[i].badge.totalHrs) { - + if ( + badgeOfType + && badgeOfType.totalHrs <= badgeCollection[i].badge.totalHrs + ) { removeDupBadge(personId, badgeOfType._id); badgeOfType = badgeCollection[i].badge; } else if ( - badgeOfType && - badgeOfType.totalHrs > badgeCollection[i].badge.totalHrs + badgeOfType + && badgeOfType.totalHrs > badgeCollection[i].badge.totalHrs ) { removeDupBadge(personId, badgeCollection[i].badge._id); } else if (!badgeOfType) { @@ -1185,10 +1266,16 @@ const userHelper = function () { replaceBadge( personId, mongoose.Types.ObjectId(badgeOfType._id), - mongoose.Types.ObjectId(bdge._id) + mongoose.Types.ObjectId(bdge._id), ); - removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); + removePrevHrBadge( + personId, + user, + badgeCollection, + bdge.hrs, + bdge.weeks, + ); } else if (!badgeOfType) { addBadge(personId, mongoose.Types.ObjectId(bdge._id)); removePrevHrBadge( @@ -1196,12 +1283,20 @@ const userHelper = function () { user, badgeCollection, bdge.hrs, - bdge.weeks + bdge.weeks, ); } else if (badgeOfType && badgeOfType.totalHrs === bdge.hrs) { - increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType._id)); - removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); - + increaseBadgeCount( + personId, + mongoose.Types.ObjectId(badgeOfType._id), + ); + removePrevHrBadge( + personId, + user, + badgeCollection, + bdge.hrs, + bdge.weeks, + ); } return false; } @@ -1214,16 +1309,25 @@ const userHelper = function () { // 'Lead a team of X+' - - const checkLeadTeamOfXplus = async function (personId, user, badgeCollection) { - const leaderRoles = ['Mentor', 'Manager', 'Administrator', 'Owner', 'Core Team']; + const checkLeadTeamOfXplus = async function ( + personId, + user, + badgeCollection, + ) { + const leaderRoles = [ + 'Mentor', + 'Manager', + 'Administrator', + 'Owner', + 'Core Team', + ]; const approvedRoles = ['Mentor', 'Manager']; if (!approvedRoles.includes(user.role)) return; let teamMembers; await getTeamMembers({ - _id: personId - }).then(results => { + _id: personId, + }).then((results) => { if (results) { teamMembers = results.myteam; } else { @@ -1242,13 +1346,17 @@ const userHelper = function () { }); let badgeOfType; for (let i = 0; i < badgeCollection.length; i += 1) { - if (badgeCollection[i].badge?.type === 'Lead a team of X+') { - if (badgeOfType && badgeOfType.people <= badgeCollection[i].badge.people) { + if ( + badgeOfType + && badgeOfType.people <= badgeCollection[i].badge.people + ) { removeDupBadge(personId, badgeOfType._id); badgeOfType = badgeCollection[i].badge; - } else if (badgeOfType && badgeOfType.people > badgeCollection[i].badge.people) { - + } else if ( + badgeOfType + && badgeOfType.people > badgeCollection[i].badge.people + ) { removeDupBadge(personId, badgeCollection[i].badge._id); } else if (!badgeOfType) { badgeOfType = badgeCollection[i].badge; @@ -1256,9 +1364,9 @@ const userHelper = function () { } } await badge - .find({ type: "Lead a team of X+" }) + .find({ type: 'Lead a team of X+' }) .sort({ people: -1 }) - .then(results => { + .then((results) => { if (!Array.isArray(results) || !results.length) { return; } @@ -1268,14 +1376,12 @@ const userHelper = function () { if ( badgeOfType._id.toString() !== badge._id.toString() && badgeOfType.people < badge.people - ) { replaceBadge( personId, mongoose.Types.ObjectId(badgeOfType._id), mongoose.Types.ObjectId(badge._id), - ); } return false; @@ -1292,40 +1398,39 @@ const userHelper = function () { const checkTotalHrsInCat = async function (personId, user, badgeCollection) { const hoursByCategory = user.hoursByCategory || {}; const categories = [ - "food", - "energy", - "housing", - "education", - "society", - "economics", - "stewardship" + 'food', + 'energy', + 'housing', + 'education', + 'society', + 'economics', + 'stewardship', ]; const badgesOfType = badgeCollection - .filter(object => object.badge.type === "Total Hrs in Category") + .filter(object => object.badge.type === 'Total Hrs in Category') .map(object => object.badge); - categories.forEach(async category => { + categories.forEach(async (category) => { const categoryHrs = Object.keys(hoursByCategory).find( - elem => elem === category + elem => elem === category, ); - let badgeOfType; for (let i = 0; i < badgeCollection.length; i += 1) { if ( - badgeCollection[i].badge?.type === "Total Hrs in Category" && - badgeCollection[i].badge?.category === category + badgeCollection[i].badge?.type === 'Total Hrs in Category' + && badgeCollection[i].badge?.category === category ) { if ( - badgeOfType && - badgeOfType.totalHrs <= badgeCollection[i].badge.totalHrs + badgeOfType + && badgeOfType.totalHrs <= badgeCollection[i].badge.totalHrs ) { removeDupBadge(personId, badgeOfType._id); badgeOfType = badgeCollection[i].badge; } else if ( - badgeOfType && - badgeOfType.totalHrs > badgeCollection[i].badge.totalHrs + badgeOfType + && badgeOfType.totalHrs > badgeCollection[i].badge.totalHrs ) { removeDupBadge(personId, badgeCollection[i].badge._id); } else if (!badgeOfType) { @@ -1336,19 +1441,19 @@ const userHelper = function () { const newCatg = category.charAt(0).toUpperCase() + category.slice(1); - - await badge.find({ type: 'Total Hrs in Category', category: newCatg }) + await badge + .find({ type: 'Total Hrs in Category', category: newCatg }) .sort({ totalHrs: -1 }) - .then(results => { + .then((results) => { if (!Array.isArray(results) || !results.length || !categoryHrs) { return; } - results.every(elem => { + results.every((elem) => { if ( - hoursByCategory[categoryHrs] >= 100 && - hoursByCategory[categoryHrs] >= elem.totalHrs + hoursByCategory[categoryHrs] >= 100 + && hoursByCategory[categoryHrs] >= elem.totalHrs ) { let theBadge; for (let i = 0; i < badgesOfType.length; i += 1) { @@ -1363,13 +1468,13 @@ const userHelper = function () { } if (badgeOfType) { if ( - badgeOfType._id.toString() !== elem._id.toString() && - badgeOfType.totalHrs < elem.totalHrs + badgeOfType._id.toString() !== elem._id.toString() + && badgeOfType.totalHrs < elem.totalHrs ) { replaceBadge( personId, mongoose.Types.ObjectId(badgeOfType._id), - mongoose.Types.ObjectId(elem._id) + mongoose.Types.ObjectId(elem._id), ); } return false; @@ -1384,11 +1489,12 @@ const userHelper = function () { }; const awardNewBadges = async () => { - console.log("Awarding"); + console.log('Awarding'); try { - - const users = await userProfile.find({ isActive: true }).populate('badgeCollection.badge'); - + // This will be used in production to run task on all users + const users = await userProfile + .find({ isActive: true }) + .populate('badgeCollection.badge'); for (let i = 0; i < users.length; i += 1) { const user = users[i]; @@ -1401,6 +1507,10 @@ const userHelper = function () { await checkLeadTeamOfXplus(personId, user, badgeCollection); await checkXHrsForXWeeks(personId, user, badgeCollection); await checkNoInfringementStreak(personId, user, badgeCollection); + // remove cache after badge asssignment. + if (cache.hasCache(`user-${_id}`)) { + cache.removeCache(`user-${_id}`); + } } } catch (err) { logger.logException(err); @@ -1410,23 +1520,28 @@ const userHelper = function () { const getTangibleHoursReportedThisWeekByUserId = function (personId) { const userId = mongoose.Types.ObjectId(personId); - const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); - const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); - + const pdtstart = moment() + .tz('America/Los_Angeles') + .startOf('week') + .format('YYYY-MM-DD'); + const pdtend = moment() + .tz('America/Los_Angeles') + .endOf('week') + .format('YYYY-MM-DD'); return timeEntries .find( { personId: userId, dateOfWork: { $gte: pdtstart, $lte: pdtend }, - isTangible: true + isTangible: true, }, - "totalSeconds" + 'totalSeconds', ) - .then(results => { + .then((results) => { const totalTangibleWeeklySeconds = results.reduce( (acc, { totalSeconds }) => acc + totalSeconds, - 0 + 0, ); return (totalTangibleWeeklySeconds / 3600).toFixed(2); }); @@ -1436,25 +1551,29 @@ const userHelper = function () { try { const users = await userProfile.find( { isActive: true, endDate: { $exists: true } }, - "_id isActive endDate" + '_id isActive endDate', ); for (let i = 0; i < users.length; i += 1) { const user = users[i]; const { endDate } = user; endDate.setHours(endDate.getHours() + 7); - if (moment().isAfter(moment(endDate).add(1, "days"))) { + if (moment().isAfter(moment(endDate).add(1, 'days'))) { await userProfile.findByIdAndUpdate( user._id, user.set({ - isActive: false + isActive: false, }), - { new: true } + { new: true }, ); const id = user._id; const person = await userProfile.findById(id); const lastDay = moment(person.endDate).format('YYYY-MM-DD'); - logger.logInfo(`User with id: ${user._id} was de-acticated at ${moment().format()}.`); + logger.logInfo( + `User with id: ${ + user._id + } was de-acticated at ${moment().format()}.`, + ); const subject = `IMPORTANT:${person.firstName} ${person.lastName} has been deactivated in the Highest Good Network`; @@ -1468,8 +1587,14 @@ const userHelper = function () {The HGN A.I. (and One Community)
`; - - emailSender('onecommunityglobal@gmail.com', subject, emailBody, null, null); + emailSender( + 'onecommunityglobal@gmail.com', + subject, + emailBody, + null, + null, + person.email, + ); } } } catch (err) { @@ -1485,10 +1610,10 @@ const userHelper = function () { } catch (error) { logger.logException(err); } - } - + }; return { + changeBadgeCount, getUserName, getTeamMembers, validateProfilePic, @@ -1502,7 +1627,7 @@ const userHelper = function () { emailWeeklySummariesForAllUsers, awardNewBadges, getTangibleHoursReportedThisWeekByUserId, - deleteExpiredTokens + deleteExpiredTokens, }; }; diff --git a/src/models/REAL_TIME_timer.js b/src/models/REAL_TIME_timer.js deleted file mode 100644 index 4e143aeaa..000000000 --- a/src/models/REAL_TIME_timer.js +++ /dev/null @@ -1,13 +0,0 @@ -const mongoose = require('mongoose'); - -const { Schema } = mongoose; - -const timerSchema = new Schema({ - userId: { type: Schema.Types.ObjectId, required: true, ref: 'userProfile' }, - totalSeconds: { type: Number, default: 0 }, - isRunning: { type: Boolean, default: false }, - isUserPaused: { type: Boolean, default: false }, - isApplicationPaused: { type: Boolean, default: false }, -}); - -module.exports = mongoose.model('newTimer', timerSchema, 'newTimers'); diff --git a/src/models/badge.js b/src/models/badge.js index 8b2c754e7..e3e93eaaf 100644 --- a/src/models/badge.js +++ b/src/models/badge.js @@ -19,7 +19,7 @@ const Badge = new Schema({ imageUrl: { type: String }, ranking: { type: Number }, description: { type: String }, - showReport: {type: Boolean}, + showReport: { type: Boolean }, }); module.exports = mongoose.model('badge', Badge, 'badges'); diff --git a/src/models/bmdashboard/buildingInventoryItem.js b/src/models/bmdashboard/buildingInventoryItem.js new file mode 100644 index 000000000..fdcfde3dd --- /dev/null +++ b/src/models/bmdashboard/buildingInventoryItem.js @@ -0,0 +1,148 @@ +const mongoose = require('mongoose'); + +//----------------------- +// BASE INVENTORY SCHEMAS +//----------------------- + +// TODO: purchaseRecord subdocs may be changed to purchaseRequests. A new purchaseRecord subdoc may be added to track purchases and costs for the item. + +// SMALL ITEMS BASE +// base schema for Consumable, Material, Reusable +// documents stored in 'buildingInventoryItems' collection + +const smallItemBaseSchema = mongoose.Schema({ + itemType: { type: mongoose.SchemaTypes.ObjectId, ref: 'invTypeBase' }, + project: { type: mongoose.SchemaTypes.ObjectId, ref: 'buildingProject' }, + stockBought: { type: Number, default: 0 }, // total amount of item bought for use in the project + // TODO: can stockAvailable default be a function? + stockAvailable: { type: Number, default: 0 }, // available = bought - (used + wasted/destroyed) + purchaseRecord: [{ + _id: false, // do not add _id field to subdocument + date: { type: Date, default: Date.now() }, + requestedBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + quantity: { type: Number, required: true, default: 1 }, // default 1 for tool or equipment purchases + priority: { type: String, enum: ['Low', 'Medium', 'High'], required: true }, + brandPref: String, + status: { type: String, default: 'Pending', enum: ['Approved', 'Pending', 'Rejected'] }, + }], + updateRecord: [{ + _id: false, + date: { type: Date, required: true }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + quantityUsed: { type: Number, required: true }, + quantityWasted: { type: Number, required: true }, + }], +}); + +const smallItemBase = mongoose.model('smallItemBase', smallItemBaseSchema, 'buildingInventoryItems'); + +// LARGE ITEMS BASE +// base schema for Tool, Equipment +// documents stored in 'buildingInventoryItems' collection + +const largeItemBaseSchema = mongoose.Schema({ + itemType: { type: mongoose.SchemaTypes.ObjectId, ref: 'invTypeBase' }, + project: { type: mongoose.SchemaTypes.ObjectId, ref: 'buildingProject' }, + purchaseStatus: { type: String, enum: ['Rental', 'Purchase'], required: true }, + // rental fields are required if purchaseStatus = "Rental" (hopefully correct syntax) + rentedOnDate: { type: Date, required: () => this.purchaseStatus === 'Rental' }, + rentalDueDate: { type: Date, required: () => this.purchaseStatus === 'Rental' }, + imageUrl: String, + purchaseRecord: [{ + _id: false, // do not add _id field to subdocument + date: { type: Date, default: Date.now() }, + requestedBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + priority: { type: String, enum: ['Low', 'Medium', 'High'], required: true }, + makeModelPref: String, + estTimeRequired: { type: Number, required: true }, // estimated time required on site + status: { type: String, default: 'Pending', enum: ['Approved', 'Pending', 'Rejected'] }, + }], + updateRecord: [{ // track tool condition updates + _id: false, + date: { type: Date, default: Date.now() }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + condition: { type: String, enum: ['Good', 'Needs Repair', 'Out of Order'] }, + }], + logRecord: [{ // track tool daily check in/out and responsible user + _id: false, + date: { type: Date, default: Date.now() }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + responsibleUser: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + type: { type: String, enum: ['Check In', 'Check Out'] }, + }], +}); + +const largeItemBase = mongoose.model('largeItemBase', largeItemBaseSchema, 'buildingInventoryItems'); + +//----------------- +// MATERIALS SCHEMA +//----------------- + +// inherits all properties of smallItemBaseSchema +// each document derived from this schema includes key field { __t: "material" } +// ex: sand, stone, bricks, lumber, insulation + +const buildingMaterial = smallItemBase.discriminator('material_item', new mongoose.Schema({ + stockUsed: { type: Number, default: 0 }, // stock that has been used up and cannot be reused + stockWasted: { type: Number, default: 0 }, // ruined or destroyed stock +})); + +//------------------ +// CONSUMABLE SCHEMA +//------------------ + +// inherits all properties of smallItemBaseSchema +// each document derived from this schema includes key field { __t: "consumable" } +// ex: screws, nails, staples + +const buildingConsumable = smallItemBase.discriminator('consumable_item', new mongoose.Schema({ + stockUsed: { type: Number, default: 0 }, // stock that has been used up and cannot be reused + stockWasted: { type: Number, default: 0 }, // ruined or destroyed stock +})); + +//---------------- +// REUSABLE SCHEMA +//---------------- + +// inherits all properties of smallItemBaseSchema +// each document derived from this schema includes key field { __t: "reusable" } +// ex: hammers, screwdrivers, mallets, brushes, gloves + +const buildingReusable = smallItemBase.discriminator('reusable_item', new mongoose.Schema({ + stockDestroyed: { type: Number, default: 0 }, +})); + +//------------ +// TOOL SCHEMA +//------------ + +// inherits all properties of largeItemBaseSchema +// each document derived from this schema includes key field { __t: "tool" } +// ex: power drills, wheelbarrows, shovels, jackhammers + +const buildingTool = largeItemBase.discriminator('tool_item', new mongoose.Schema({ + code: { type: Number, required: true }, // TODO: add function to create simple numeric code for on-site tool tracking +})); + + +//----------------- +// EQUIPMENT SCHEMA +//----------------- + +// inherits all properties of largeItemBaseSchema +// each document derived from this schema includes key field { __t: "equipment" } +// items in this category are assumed to be rented +// ex: tractors, excavators, bulldozers + +const buildingEquipment = largeItemBase.discriminator('equipment_item', new mongoose.Schema({ + isTracked: { type: Boolean, required: true }, // has asset tracker + assetTracker: { type: String, required: () => this.isTracked }, // required if isTracked = true (syntax?) +})); + +module.exports = { + buildingMaterial, + buildingConsumable, + buildingReusable, + buildingTool, + buildingEquipment, +}; diff --git a/src/models/bmdashboard/buildingInventoryType.js b/src/models/bmdashboard/buildingInventoryType.js new file mode 100644 index 000000000..bd125dfd3 --- /dev/null +++ b/src/models/bmdashboard/buildingInventoryType.js @@ -0,0 +1,83 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +//--------------------------- +// BASE INVENTORY TYPE SCHEMA +//--------------------------- + +// all schemas will inherit these properties +// all documents will live in buildingInventoryTypes collection + +const invTypeBaseSchema = new Schema({ + name: { type: String, required: true }, + description: { type: String, required: true, maxLength: 150 }, + imageUrl: String, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfiles' }, +}); + +const invTypeBase = mongoose.model('invTypeBase', invTypeBaseSchema, 'buildingInventoryTypes'); + +//--------------------------- +// MATERIAL TYPE +//--------------------------- + +// ex: sand, stone, brick, lumber + +const materialType = invTypeBase.discriminator('material_type', new mongoose.Schema({ + category: { type: String, enum: ['Material'] }, + unit: { type: String, required: true }, // unit of measurement +})); + +//--------------------------- +// CONSUMABLE TYPE +//--------------------------- + +// ex: screws, nails, staples + +const consumableType = invTypeBase.discriminator('consumable_type', new mongoose.Schema({ + category: { type: String, enum: ['Consumable'] }, + size: { type: String, required: true }, +})); + +//--------------------------- +// REUSABLE TYPE +//--------------------------- + +// ex: gloves, brushes, hammers, screwdrivers + +const reusableType = invTypeBase.discriminator('reusable_type', new mongoose.Schema({ + category: { type: String, enum: ['Reusable'] }, +})); + +//--------------------------- +// TOOL TYPE +//--------------------------- + +// ex: shovels, wheelbarrows, power drills, jackhammers + +const toolType = invTypeBase.discriminator('tool_type', new mongoose.Schema({ + category: { type: String, enum: ['Tool'] }, + isPowered: { type: Boolean, required: true }, + powerSource: { type: String, required: () => this.isPowered }, // required if isPowered = true (syntax?) +})); + +//--------------------------- +// EQUIPMENT TYPE +//--------------------------- + +// ex: tractors, excavators + +const equipmentType = invTypeBase.discriminator('equipment_type', new mongoose.Schema({ + category: { type: String, enum: ['Equipment'] }, + fuelType: { type: String, enum: ['Diesel', 'Biodiesel', 'Gasoline', 'Natural Gas', 'Ethanol'], required: true }, +})); + +module.exports = { + invTypeBase, + materialType, + consumableType, + reusableType, + toolType, + equipmentType, +}; \ No newline at end of file diff --git a/src/models/bmdashboard/buildingMaterial.js b/src/models/bmdashboard/buildingMaterial.js new file mode 100644 index 000000000..bc86884ed --- /dev/null +++ b/src/models/bmdashboard/buildingMaterial.js @@ -0,0 +1,29 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const buildingMaterial = new Schema({ + itemType: { type: mongoose.SchemaTypes.ObjectId, ref: 'buildingInventoryType' }, + project: { type: mongoose.SchemaTypes.ObjectId, ref: 'buildingProject' }, + stockBought: { type: Number, default: 0 }, // total amount of item bought for use in the project + stockUsed: { type: Number, default: 0 }, // total amount of item used successfully in the project + stockWasted: { type: Number, default: 0 }, // total amount of item wasted/ruined/lost in the project + stockAvailable: { type: Number, default: 0 }, // bought - (used + wasted) + purchaseRecord: [{ + _id: false, // do not add _id field to subdocument + date: { type: Date, default: Date.now() }, + requestedBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + quantity: { type: Number, required: true }, + priority: { type: String, enum: ['Low', 'Medium', 'High'], required: true }, + brand: String, + status: { type: String, default: 'Pending', enum: ['Approved', 'Pending', 'Rejected'] }, + }], + updateRecord: [{ + _id: false, + date: { type: Date, required: true }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + quantityUsed: { type: Number, required: true }, + quantityWasted: { type: Number, required: true }, + }], +}); +module.exports = mongoose.model('buildingMaterial', buildingMaterial, 'buildingMaterials'); diff --git a/src/models/bmdashboard/buildingProject.js b/src/models/bmdashboard/buildingProject.js new file mode 100644 index 000000000..3ca4bf993 --- /dev/null +++ b/src/models/bmdashboard/buildingProject.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const buildingProject = new Schema({ + isActive: Boolean, + name: String, + template: String, // construction template (ie Earthbag Village) + location: String, // use lat/lng instead? + dateCreated: { type: Date, default: Date.now }, + buildingManager: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + teams: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'teams' }], // teams assigned to the project + members: [{ + user: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + hours: { type: Number, default: 0 }, // tracked via the Member Check-In Page timer + }], +}); + +module.exports = mongoose.model('buildingProject', buildingProject, 'buildingProjects'); diff --git a/src/models/bmdashboard/buildingTool.js b/src/models/bmdashboard/buildingTool.js new file mode 100644 index 000000000..95be0c4d5 --- /dev/null +++ b/src/models/bmdashboard/buildingTool.js @@ -0,0 +1,36 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const buildingTool = new Schema({ + itemType: { type: mongoose.SchemaTypes.ObjectId, ref: 'buildingInventoryType' }, + code: { type: Number, required: true }, // add function to create code for on-site tool tracking + purchaseStatus: { type: String, enum: ['Rental', 'Purchase'], required: true }, + // add discriminator based on rental or purchase so these fields are required if tool is rented + rentedOnDate: Date, + rentalDue: Date, + userResponsible: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + purchaseRecord: [{ // track purchase/rental requests + _id: false, // do not add _id field to subdocument + date: { type: Date, default: Date.now() }, + requestedBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + priority: { type: String, enum: ['Low', 'Medium', 'High'], required: true }, + brand: String, + status: { type: String, default: 'Pending', enum: ['Approved', 'Pending', 'Rejected'] }, + }], + updateRecord: [{ // track tool condition updates + _id: false, + date: { type: Date, default: Date.now() }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + condition: { type: String, enum: ['Good', 'Needs Repair', 'Out of Order'] }, + }], + logRecord: [{ // track tool daily check in/out and use + _id: false, + date: { type: Date, default: Date.now() }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + responsibleUser: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + type: { type: String, enum: ['Check In', 'Check Out'] }, // default = opposite of current log status? + }], +}); + +module.exports = mongoose.model('buildingTool', buildingTool, 'buildingTools'); diff --git a/src/models/inventoryItemMaterial.js b/src/models/inventoryItemMaterial.js index e6153af45..7cba95db6 100644 --- a/src/models/inventoryItemMaterial.js +++ b/src/models/inventoryItemMaterial.js @@ -6,10 +6,10 @@ const InventoryItemMaterial = new Schema({ inventoryItemType: { type: mongoose.SchemaTypes.ObjectId, ref: 'inventoryItemType', required: true }, project: { type: mongoose.SchemaTypes.ObjectId, ref: 'project', required: true }, stockBought: { type: Number, required: true }, // amount bought for project, affects total stock - stockUsed: { type: Number, required: true }, + stockUsed: { type: Number, default: 0 }, stockAvailable: { type: Number, required: true }, - stockHeld: { type: Number, required: true }, - stockWasted: { type: Number, required: true }, + stockHeld: { type: Number, default: 0 }, + stockWasted: { type: Number, default: 0 }, usageRecord: [{ // daily log of amount inventory item used at job site date: { type: Date, required: true, default: Date.now() }, createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile', required: true }, @@ -29,12 +29,12 @@ const InventoryItemMaterial = new Schema({ poId: { type: String, required: true }, sellerId: { type: String, required: true }, quantity: { type: Number, required: true }, // adds to stockBought - unitPrice: {type: Number, required: true}, + unitPrice: { type: Number, required: true }, currency: { type: String, required: true }, subtotal: { type: Number, required: true }, tax: { type: Number, required: true }, shipping: { type: Number, required: true }, - }] -}) + }], +}); module.exports = mongoose.model('inventoryItemMaterial', InventoryItemMaterial, 'inventoryMaterial'); diff --git a/src/models/inventoryItemType.js b/src/models/inventoryItemType.js index 321038a84..b7b3ec46f 100644 --- a/src/models/inventoryItemType.js +++ b/src/models/inventoryItemType.js @@ -9,11 +9,9 @@ const InventoryItemType = new Schema({ // creates an item, tracks total amount i uom: { type: String, required: true }, // unit of measurement totalStock: { type: Number, required: true }, // total amount of all stock acquired totalAvailable: { type: Number, required: true }, - projectsUsing: [ {type: mongoose.SchemaTypes.ObjectId, ref: 'project'} ], + projectsUsing: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'project' }], imageUrl: { type: String }, - link: { type: String} + link: { type: String }, }); module.exports = mongoose.model('inventoryItemType', InventoryItemType, 'inventoryItemType'); - - diff --git a/src/models/mapLocation.js b/src/models/mapLocation.js new file mode 100644 index 000000000..cb5644d31 --- /dev/null +++ b/src/models/mapLocation.js @@ -0,0 +1,43 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const mapLocation = new Schema({ + title: { + type: String, + default: 'Prior to HGN Data Collection', + }, + firstName: String, + lastName: String, + jobTitle: String, + isActive: { + type: Boolean, + default: false, + }, + location: { + userProvided: { + type: String, + required: true, + }, + coords: { + lat: { + type: String, + required: true, + }, + lng: { + type: String, + required: true, + }, + }, + country: { + type: String, + required: true, + }, + city: { + type: String, + default: '', + }, + }, +}); + +module.exports = mongoose.model('MapLocation', mapLocation, 'maplocations'); diff --git a/src/models/oldTimer.js b/src/models/oldTimer.js deleted file mode 100644 index dca0ade1a..000000000 --- a/src/models/oldTimer.js +++ /dev/null @@ -1,14 +0,0 @@ -const mongoose = require('mongoose'); - -const { Schema } = mongoose; - -const timerSchema = new Schema({ - userId: { type: Schema.Types.ObjectId, required: true, ref: 'userProfile' }, - pausedAt: { type: Number, default: 0 }, - isWorking: { type: Boolean, default: false }, - started: { type: Date }, - lastAccess: { type: Date }, - }); - - -module.exports = mongoose.model('timer', timerSchema, 'timers'); diff --git a/src/models/ownerMessage.js b/src/models/ownerMessage.js index be953c3a3..a6314c929 100644 --- a/src/models/ownerMessage.js +++ b/src/models/ownerMessage.js @@ -3,7 +3,8 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; const OwnerMessage = new Schema({ - message: { type: String }, + message: { type: String, default: '' }, + standardMessage: { type: String, default: '' }, }); module.exports = mongoose.model('ownerMessage', OwnerMessage, 'ownerMessage'); diff --git a/src/models/ownerStandardMessage.js b/src/models/ownerStandardMessage.js deleted file mode 100644 index d344a773f..000000000 --- a/src/models/ownerStandardMessage.js +++ /dev/null @@ -1,9 +0,0 @@ -const mongoose = require('mongoose'); - -const { Schema } = mongoose; - -const OwnerStandardMessage = new Schema({ - message: { type: String }, -}); - -module.exports = mongoose.model('ownerStandardMessage', OwnerStandardMessage, 'ownerStandardMessage'); diff --git a/src/models/profileInitialSetupToken.js b/src/models/profileInitialSetupToken.js index 48413fb77..77b830229 100644 --- a/src/models/profileInitialSetupToken.js +++ b/src/models/profileInitialSetupToken.js @@ -10,7 +10,7 @@ const profileInitialSetupTokenSchema = new mongoose.Schema({ type: String, required: true, }, - weeklyCommittedHours : { + weeklyCommittedHours: { type: Number, required: true, default: 10, @@ -21,4 +21,4 @@ const profileInitialSetupTokenSchema = new mongoose.Schema({ }, }); -module.exports = mongoose.model('profileInitialSetupToken', profileInitialSetupTokenSchema, 'profileInitialSetupToken'); \ No newline at end of file +module.exports = mongoose.model('profileInitialSetupToken', profileInitialSetupTokenSchema, 'profileInitialSetupToken'); diff --git a/src/models/team.js b/src/models/team.js index 8d46db283..1df50b95a 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -5,7 +5,7 @@ const { Schema } = mongoose; const team = new Schema({ teamName: { type: 'String', required: true }, isActive: { type: 'Boolean', required: true, default: true }, - createdDatetime: { type: Date }, + createdDatetime: { type: Date, default: Date.now() }, modifiedDatetime: { type: Date, default: Date.now() }, members: [ { @@ -13,6 +13,18 @@ const team = new Schema({ addDateTime: { type: Date, default: Date.now(), ref: 'userProfile' }, }, ], + teamCode: { + type: 'String', + default: '', + validate: { + validator(v) { + const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$|^$/; + return teamCoderegex.test(v); + }, + message: + 'Please enter a code in the format of A-AAA or AAAAA', + }, + }, }); module.exports = mongoose.model('team', team, 'teams'); diff --git a/src/models/timeentry.js b/src/models/timeentry.js index aeef5fdc7..79504cc20 100644 --- a/src/models/timeentry.js +++ b/src/models/timeentry.js @@ -4,8 +4,12 @@ const { Schema } = mongoose; const TimeEntry = new Schema({ - personId: { type: Schema.Types.ObjectId, required: [true, 'Resource is a required field'], ref: 'userProfile' }, - projectId: { type: Schema.Types.ObjectId, required: [true, 'Project is a required field'], ref: 'project' }, + entryType: { type: String, required: true, default: 'default' }, + personId: { type: Schema.Types.ObjectId, ref: 'userProfile' }, + projectId: { type: Schema.Types.ObjectId, ref: 'project' }, + wbsId: { type: Schema.Types.ObjectId, ref: 'project' }, + taskId: { type: Schema.Types.ObjectId, default: null, ref: 'wbs' }, + teamId: { type: Schema.Types.ObjectId, ref: 'task' }, dateOfWork: { type: String, required: true }, totalSeconds: { type: Number }, notes: { type: String }, diff --git a/src/models/timer.js b/src/models/timer.js index c73dfe6c2..f50921fb3 100644 --- a/src/models/timer.js +++ b/src/models/timer.js @@ -5,13 +5,14 @@ const { Schema } = mongoose; const timerSchema = new Schema({ userId: { type: Schema.Types.ObjectId, required: true, ref: "userProfile" }, - lastAccess: { type: Date, default: Date.now }, + startAt: { type: Date, default: Date.now }, time: { type: Number, default: 900000 }, - countdown: { type: Boolean, default: true }, goal: { type: Number, default: 900000 }, - paused: { type: Boolean, default: true }, + initialGoal: { type: Number, default: 900000 }, + chiming: { type: Boolean, default: false }, + paused: { type: Boolean, default: false }, forcedPause: { type: Boolean, default: false }, - stopped: { type: Boolean, default: false }, + started: { type: Boolean, default: false }, }); module.exports = mongoose.model("newTimer", timerSchema, "newTimers"); diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 483eb228f..633a81026 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -1,13 +1,13 @@ -const mongoose = require('mongoose'); -const moment = require('moment-timezone'); +const mongoose = require("mongoose"); +const moment = require("moment-timezone"); const { Schema } = mongoose; -const validate = require('mongoose-validator'); -const bcrypt = require('bcryptjs'); +const validate = require("mongoose-validator"); +const bcrypt = require("bcryptjs"); const SALT_ROUNDS = 10; const nextDay = new Date(); -nextDay.setDate(nextDay.getDate()+1); +nextDay.setDate(nextDay.getDate() + 1); const userProfileSchema = new Schema({ password: { @@ -15,11 +15,12 @@ const userProfileSchema = new Schema({ required: true, validate: { validator(v) { - const passwordregex = /(?=^.{8,}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/; + const passwordregex = + /(?=^.{8,}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/; return passwordregex.test(v); }, message: - '{VALUE} is not a valid password!password should be at least 8 charcaters long with uppercase, lowercase and number/special char.', + "{VALUE} is not a valid password!password should be at least 8 charcaters long with uppercase, lowercase and number/special char.", }, }, isActive: { type: Boolean, required: true, default: true }, @@ -47,7 +48,9 @@ const userProfileSchema = new Schema({ type: String, required: true, unique: true, - validate: [validate({ validator: 'isEmail', message: 'Email address is invalid' })], + validate: [ + validate({ validator: "isEmail", message: "Email address is invalid" }), + ], }, weeklycommittedHours: { type: Number, default: 10 }, weeklycommittedHoursHistory: [ @@ -60,13 +63,15 @@ const userProfileSchema = new Schema({ createdDate: { type: Date, required: true, default: nextDay }, lastModifiedDate: { type: Date, required: true, default: Date.now() }, reactivationDate: { type: Date }, - personalLinks: [{ _id: Schema.Types.ObjectId, Name: String, Link: { type: String } }], + personalLinks: [ + { _id: Schema.Types.ObjectId, Name: String, Link: { type: String } }, + ], adminLinks: [{ _id: Schema.Types.ObjectId, Name: String, Link: String }], - teams: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'team' }], - projects: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'project' }], + teams: [{ type: mongoose.SchemaTypes.ObjectId, ref: "team" }], + projects: [{ type: mongoose.SchemaTypes.ObjectId, ref: "project" }], badgeCollection: [ { - badge: { type: mongoose.SchemaTypes.ObjectId, ref: 'badge' }, + badge: { type: mongoose.SchemaTypes.ObjectId, ref: "badge" }, count: { type: Number, default: 0 }, earnedDate: { type: Array, default: [] }, lastModified: { type: Date, required: true, default: Date.now() }, @@ -79,11 +84,29 @@ const userProfileSchema = new Schema({ ], profilePic: { type: String }, infringements: [ - { date: { type: String, required: true }, description: { type: String, required: true } }, + { + date: { type: String, required: true }, + description: { type: String, required: true }, + }, ], - location: { type: String, default: '' }, + location: { + userProvided: { type: String, default: "" }, + coords: { + lat: { type: Number, default: "" }, + lng: { type: Number, default: "" }, + }, + country: { type: String, default: "" }, + city: { type: String, default: "" }, + }, oldInfringements: [ - { date: { type: String, required: true }, description: { type: String, required: true } }, + { + date: { type: String, required: true }, + description: { type: String, required: true }, + }, + { + date: { type: String, required: true }, + description: { type: String, required: true }, + }, ], privacySettings: { blueSquares: { type: Boolean, default: true }, @@ -95,7 +118,7 @@ const userProfileSchema = new Schema({ dueDate: { type: Date, required: true, - default: moment().tz('America/Los_Angeles').endOf('week'), + default: moment().tz("America/Los_Angeles").endOf("week"), }, summary: { type: String }, uploadDate: { type: Date }, @@ -125,17 +148,17 @@ const userProfileSchema = new Schema({ category: { type: String, enum: [ - 'Food', - 'Energy', - 'Housing', - 'Education', - 'Society', - 'Economics', - 'Stewardship', - 'Other', - 'Unspecified', + "Food", + "Energy", + "Housing", + "Education", + "Society", + "Economics", + "Stewardship", + "Other", + "Unspecified", ], - default: 'Other', + default: "Other", }, hrs: { type: Number, default: 0 }, }, @@ -143,39 +166,60 @@ const userProfileSchema = new Schema({ savedTangibleHrs: [Number], timeEntryEditHistory: [ { - date: { type: Date, required: true, default: moment().tz('America/Los_Angeles').toDate() }, + date: { + type: Date, + required: true, + default: moment().tz("America/Los_Angeles").toDate(), + }, initialSeconds: { type: Number, required: true }, newSeconds: { type: Number, required: true }, }, ], weeklySummaryNotReq: { type: Boolean, default: false }, - timeZone: { type: String, required: true, default: 'America/Los_Angeles' }, + timeZone: { type: String, required: true, default: "America/Los_Angeles" }, isVisible: { type: Boolean, default: false }, weeklySummaryOption: { type: String }, - bioPosted: { type: String, default: 'default' }, - isFirstTimelog: { type: Boolean, default: true}, - teamCode: { type: String, default: '' }, + bioPosted: { type: String, default: "default" }, + isFirstTimelog: { type: Boolean, default: true }, + teamCode: { + type: String, + default: "", + validate: { + validator(v) { + const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$|^$/; + return teamCoderegex.test(v); + }, + message: "Please enter a code in the format of A-AAA or AAAAA", + }, + }, infoCollections: [ { - areaName: { type: String }, + areaName: { type: String }, areaContent: { type: String }, - }], + }, + ], // actualEmail field represents the actual email associated with a real volunteer in the main HGN app. actualEmail is required for Administrator and Owner accounts only in the dev environment. actualEmail: { type: String }, + timeOffFrom: { type: Date, default: undefined }, + timeOffTill: { type: Date, default: undefined }, }); -userProfileSchema.pre('save', function (next) { +userProfileSchema.pre("save", function (next) { const user = this; - if (!user.isModified('password')) return next(); + if (!user.isModified("password")) return next(); return bcrypt .genSalt(SALT_ROUNDS) - .then(result => bcrypt.hash(user.password, result)) + .then((result) => bcrypt.hash(user.password, result)) .then((hash) => { user.password = hash; return next(); }) - .catch(error => next(error)); + .catch((error) => next(error)); }); -module.exports = mongoose.model('userProfile', userProfileSchema, 'userProfiles'); +module.exports = mongoose.model( + "userProfile", + userProfileSchema, + "userProfiles" +); diff --git a/src/models/weeklySummaryAIPrompt.js b/src/models/weeklySummaryAIPrompt.js new file mode 100644 index 000000000..a55db4a97 --- /dev/null +++ b/src/models/weeklySummaryAIPrompt.js @@ -0,0 +1,10 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const WeeklySummaryAIPrompt = new Schema({ + _id: { type: mongoose.Schema.Types.String }, + aIPromptText: { type: String }, +}); + +module.exports = mongoose.model('weeklySummaryAIPrompt', WeeklySummaryAIPrompt, 'weeklySummaryAIPrompt'); diff --git a/src/routes/bmdashboard/bmConsumablesRouter.js b/src/routes/bmdashboard/bmConsumablesRouter.js new file mode 100644 index 000000000..51126e3a3 --- /dev/null +++ b/src/routes/bmdashboard/bmConsumablesRouter.js @@ -0,0 +1,13 @@ +const express = require('express'); + +const routes = function (BuildingConsumable) { + const BuildingConsumableController = express.Router(); + const controller = require('../../controllers/bmdashboard/bmConsumableController')(BuildingConsumable); + + BuildingConsumableController.route('/consumables') + .get(controller.fetchBMConsumables); + + return BuildingConsumableController; +}; + +module.exports = routes; diff --git a/src/routes/bmdashboard/bmInventoryTypeRouter.js b/src/routes/bmdashboard/bmInventoryTypeRouter.js new file mode 100644 index 000000000..e89cb6b74 --- /dev/null +++ b/src/routes/bmdashboard/bmInventoryTypeRouter.js @@ -0,0 +1,21 @@ +const express = require('express'); + +const routes = function (baseInvType, matType, consType, reusType, toolType, equipType) { + const inventoryTypeRouter = express.Router(); + const controller = require('../../controllers/bmdashboard/bmInventoryTypeController')(baseInvType, matType, consType, reusType, toolType, equipType); + + // Route for fetching all material types + inventoryTypeRouter.route('/invtypes/materials') + .get(controller.fetchMaterialTypes); + + inventoryTypeRouter.route('/invtypes/equipment') + .post(controller.addEquipmentType); + + // Combined routes for getting a single inventory type and updating its name and unit of measurement + inventoryTypeRouter.route('/invtypes/material/:invtypeId') + .get(controller.fetchSingleInventoryType) + .put(controller.updateNameAndUnit); + return inventoryTypeRouter; +}; + +module.exports = routes; diff --git a/src/routes/bmdashboard/bmMaterialsRouter.js b/src/routes/bmdashboard/bmMaterialsRouter.js index ab8a67388..733148d14 100644 --- a/src/routes/bmdashboard/bmMaterialsRouter.js +++ b/src/routes/bmdashboard/bmMaterialsRouter.js @@ -1,13 +1,20 @@ const express = require('express'); -const routes = function (itemMaterial) { -const materialsRouter = express.Router(); -const controller = require('../../controllers/bmdashboard/bmMaterialsController')(itemMaterial); +const routes = function (buildingMaterial) { + const materialsRouter = express.Router(); + const controller = require('../../controllers/bmdashboard/bmMaterialsController')(buildingMaterial); + materialsRouter.route('/materials') + .get(controller.bmMaterialsList) + .post(controller.bmPurchaseMaterials); + + materialsRouter.route('/updateMaterialRecord') + .post(controller.bmPostMaterialUpdateRecord); + + materialsRouter.route('/updateMaterialRecordBulk') + .post(controller.bmPostMaterialUpdateBulk); -materialsRouter.route('/materials') - .get(controller.bmMaterialsList); return materialsRouter; -} +}; -module.exports = routes; \ No newline at end of file +module.exports = routes; diff --git a/src/routes/bmdashboard/bmProjectRouter.js b/src/routes/bmdashboard/bmProjectRouter.js new file mode 100644 index 000000000..d60ea9b2b --- /dev/null +++ b/src/routes/bmdashboard/bmProjectRouter.js @@ -0,0 +1,16 @@ +const express = require('express'); + +const routes = function (buildingProject) { + const projectRouter = express.Router(); + const controller = require('../../controllers/bmdashboard/bmProjectController')(buildingProject); + +projectRouter.route('/projects') + .get(controller.fetchAllProjects); + +projectRouter.route('/project/:projectId') + .get(controller.fetchSingleProject); + + return projectRouter; +}; + +module.exports = routes; diff --git a/src/routes/bmdashboard/bmToolRouter.js b/src/routes/bmdashboard/bmToolRouter.js new file mode 100644 index 000000000..a1a30ea40 --- /dev/null +++ b/src/routes/bmdashboard/bmToolRouter.js @@ -0,0 +1,13 @@ +const express = require('express'); + +const routes = function (BuildingTool) { + const toolRouter = express.Router(); + const controller = require('../../controllers/bmdashboard/bmToolController')(BuildingTool); + + toolRouter.route('/tools/:toolId') + .get(controller.fetchSingleTool); + + return toolRouter; +}; + +module.exports = routes; diff --git a/src/routes/dashboardRouter.js b/src/routes/dashboardRouter.js index 33275597c..fc54a43a1 100644 --- a/src/routes/dashboardRouter.js +++ b/src/routes/dashboardRouter.js @@ -3,9 +3,12 @@ const express = require('express'); const route = function () { const controller = require('../controllers/dashBoardController')(); - const Dashboardrouter = express.Router(); + Dashboardrouter.route('/dashboard/aiPrompt') + .get(controller.getAIPrompt) + .put(controller.updateAIPrompt); + Dashboardrouter.route('/dashboard/:userId') .get(controller.dashboarddata); diff --git a/src/routes/isEmailExistsRouter.js b/src/routes/isEmailExistsRouter.js index cfb4e6033..d19b14fe2 100644 --- a/src/routes/isEmailExistsRouter.js +++ b/src/routes/isEmailExistsRouter.js @@ -2,14 +2,14 @@ const express = require('express'); const routes = function () { - const controller = require('../controllers/isEmailExistsController')() + const controller = require('../controllers/isEmailExistsController')(); - const isEmailExistsRouter = express.Router() + const isEmailExistsRouter = express.Router(); - isEmailExistsRouter.route("/is-email-exists/:email") - .get(controller.isEmailExists) + isEmailExistsRouter.route('/is-email-exists/:email') + .get(controller.isEmailExists); - return isEmailExistsRouter -} + return isEmailExistsRouter; +}; -module.exports = routes +module.exports = routes; diff --git a/src/routes/mapLocationsRouter.js b/src/routes/mapLocationsRouter.js new file mode 100644 index 000000000..84cb85feb --- /dev/null +++ b/src/routes/mapLocationsRouter.js @@ -0,0 +1,19 @@ +const express = require('express'); + +const router = function (mapLocations) { + const controller = require('../controllers/mapLocationsController')(mapLocations); + + const mapRouter = express.Router(); + + mapRouter.route('/mapLocations') + .get(controller.getAllLocations) + .put(controller.putUserLocation) + .patch(controller.updateUserLocation); + + mapRouter.route('/mapLocations/:locationId') + .delete(controller.deleteLocation); + + return mapRouter; +}; + +module.exports = router; diff --git a/src/routes/ownerMessageRouter.js b/src/routes/ownerMessageRouter.js index e436deed8..6f5716fe9 100644 --- a/src/routes/ownerMessageRouter.js +++ b/src/routes/ownerMessageRouter.js @@ -5,14 +5,11 @@ const routes = function (ownerMessage) { const OwnerMessageRouter = express.Router(); OwnerMessageRouter.route('/ownerMessage') - .post(controller.postOwnerMessage) - .get(controller.getOwnerMessage) - .delete(controller.deleteOwnerMessage); + .get(controller.getOwnerMessage) + .put(controller.updateOwnerMessage) + .delete(controller.deleteOwnerMessage); - OwnerMessageRouter.route('/ownerMessage/:id') - .put(controller.updateOwnerMessage); - -return OwnerMessageRouter; + return OwnerMessageRouter; }; module.exports = routes; diff --git a/src/routes/ownerStandardMessageRouter.js b/src/routes/ownerStandardMessageRouter.js deleted file mode 100644 index 08e629c3d..000000000 --- a/src/routes/ownerStandardMessageRouter.js +++ /dev/null @@ -1,18 +0,0 @@ -const express = require('express'); - -const routes = function (ownerStandardMessage) { - const controller = require('../controllers/ownerStandardMessageController')(ownerStandardMessage); - const OwnerStandardMessageRouter = express.Router(); - - OwnerStandardMessageRouter.route('/ownerStandardMessage') - .post(controller.postOwnerStandardMessage) - .get(controller.getOwnerStandardMessage) - .delete(controller.deleteOwnerStandardMessage); - - OwnerStandardMessageRouter.route('/ownerStandardMessage/:id') - .put(controller.updateOwnerStandardMessage); - -return OwnerStandardMessageRouter; -}; - -module.exports = routes; diff --git a/src/routes/profileInitialSetupRouter.js b/src/routes/profileInitialSetupRouter.js index a23c6a868..35dd45388 100644 --- a/src/routes/profileInitialSetupRouter.js +++ b/src/routes/profileInitialSetupRouter.js @@ -1,13 +1,14 @@ const express = require('express'); -const routes = function (ProfileInitialSetupToken, userProfile, Project) { +const routes = function (ProfileInitialSetupToken, userProfile, Project, mapLocations) { const ProfileInitialSetup = express.Router(); - const controller = require('../controllers/profileInitialSetupController')(ProfileInitialSetupToken, userProfile, Project); + const controller = require('../controllers/profileInitialSetupController')(ProfileInitialSetupToken, userProfile, Project, mapLocations); ProfileInitialSetup.route('/getInitialSetuptoken') .post(controller.getSetupToken); - ProfileInitialSetup.route('/ProfileInitialSetup').post(controller.setUpNewUser) - ProfileInitialSetup.route('/validateToken').post(controller.validateSetupToken) - ProfileInitialSetup.route('/getTimeZoneAPIKeyByToken').post(controller.getTimeZoneAPIKeyByToken) + ProfileInitialSetup.route('/ProfileInitialSetup').post(controller.setUpNewUser); + ProfileInitialSetup.route('/validateToken').post(controller.validateSetupToken); + ProfileInitialSetup.route('/getTimeZoneAPIKeyByToken').post(controller.getTimeZoneAPIKeyByToken); + ProfileInitialSetup.route('/getTotalCountryCount').get(controller.getTotalCountryCount); return ProfileInitialSetup; }; diff --git a/src/routes/taskRouter.js b/src/routes/taskRouter.js index 66e0520e3..e04b499eb 100644 --- a/src/routes/taskRouter.js +++ b/src/routes/taskRouter.js @@ -28,6 +28,9 @@ const routes = function (task, userProfile) { wbsRouter.route('/task/update/:taskId') .put(controller.updateTask); + wbsRouter.route('/task/updateStatus/:taskId') + .put(controller.updateTaskStatus); + wbsRouter.route('/task/updateAllParents/:wbsId/') .put(controller.updateAllParents); @@ -40,12 +43,15 @@ const routes = function (task, userProfile) { wbsRouter.route('/tasks/moveTasks/:wbsId') .put(controller.moveTask); - wbsRouter.route('/tasks/userProfile') - .get(controller.getTasksByUserList); + wbsRouter.route('/tasks/user/:userId') + .get(controller.getTasksByUserId); wbsRouter.route('/user/:userId/teams/tasks') .get(controller.getTasksForTeamsByUser); + wbsRouter.route('/tasks/reviewreq/:userId') + .post(controller.sendReviewReq); + return wbsRouter; }; diff --git a/src/routes/timeentryRouter.js b/src/routes/timeentryRouter.js index b319aa595..0562f49ed 100644 --- a/src/routes/timeentryRouter.js +++ b/src/routes/timeentryRouter.js @@ -19,6 +19,15 @@ const routes = function (TimeEntry) { TimeEntryRouter.route('/TimeEntry/users') .post(controller.getTimeEntriesForUsersList); + TimeEntryRouter.route('/TimeEntry/lostUsers') + .post(controller.getLostTimeEntriesForUserList); + + TimeEntryRouter.route('/TimeEntry/lostProjects') + .post(controller.getLostTimeEntriesForProjectList); + + TimeEntryRouter.route('/TimeEntry/lostTeams') + .post(controller.getLostTimeEntriesForTeamList); + TimeEntryRouter.route('/TimeEntry/projects/:projectId/:fromDate/:toDate') .get(controller.getTimeEntriesForSpecifiedProject); diff --git a/src/routes/timerRouter.js b/src/routes/timerRouter.js deleted file mode 100644 index 094b2ba81..000000000 --- a/src/routes/timerRouter.js +++ /dev/null @@ -1,15 +0,0 @@ -const express = require('express'); - -const routes = function (Timer) { - const TimerRouter = express.Router(); - - const controller = require('../controllers/REAL_TIME_timerController')(Timer); - - TimerRouter.route('/timer/:userId') - .put(controller.putTimer) - .get(controller.getTimer); - - return TimerRouter; -}; - -module.exports = routes; diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index 9032359a8..ac567ebe8 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -1,53 +1,94 @@ +import { body } from 'express-validator'; + const express = require('express'); const routes = function (userProfile) { - const controller = require('../controllers/userProfileController')(userProfile); + const controller = require('../controllers/userProfileController')( + userProfile, + ); const userProfileRouter = express.Router(); - userProfileRouter.route('/userProfile') + userProfileRouter + .route('/userProfile') .get(controller.getUserProfiles) - .post(controller.postUserProfile); - - userProfileRouter.route('/userProfile/:userId') + .post( + body('firstName').customSanitizer(value => value.trim()), + body('lastName').customSanitizer(value => value.trim()), + controller.postUserProfile, + ); + + userProfileRouter + .route('/userProfile/:userId') .get(controller.getUserById) - .put(controller.putUserProfile) + .put( + body('firstName').customSanitizer(value => value.trim()), + body('lastName').customSanitizer(value => value.trim()), + body('personalLinks').customSanitizer(value => value.map((link) => { + if (link.Name.replace(/\s/g, '') || link.Link.replace(/\s/g, '')) { + return { + ...link, + Name: link.Name.trim(), + Link: link.Link.replace(/\s/g, ''), + }; + } + throw new Error('Url not valid'); + })), + body('adminLinks').customSanitizer(value => value.map((link) => { + if (link.Name.replace(/\s/g, '') || link.Link.replace(/\s/g, '')) { + return { + ...link, + Name: link.Name.trim(), + Link: link.Link.replace(/\s/g, ''), + }; + } + throw new Error('Url not valid'); + })), + controller.putUserProfile, + ) .delete(controller.deleteUserProfile) .patch(controller.changeUserStatus); - userProfileRouter.route('/userProfile/name/:name') + userProfileRouter + .route('/userProfile/name/:name') .get(controller.getUserByName); - userProfileRouter.route('/refreshToken/:userId') - .get(controller.refreshToken); + userProfileRouter.route('/refreshToken/:userId').get(controller.refreshToken); - userProfileRouter.route('/userProfile/reportees/:userId') + userProfileRouter + .route('/userProfile/reportees/:userId') .get(controller.getreportees); - userProfileRouter.route('/userProfile/teammembers/:userId') + userProfileRouter + .route('/userProfile/teammembers/:userId') .get(controller.getTeamMembersofUser); - userProfileRouter.route('/userProfile/:userId/property') + userProfileRouter + .route('/userProfile/:userId/property') .patch(controller.updateOneProperty); - userProfileRouter.route('/userProfile/:userId/updatePassword') + userProfileRouter + .route('/userProfile/:userId/updatePassword') .patch(controller.updatepassword); - userProfileRouter.route('/userProfile/:userId/resetPassword') + userProfileRouter + .route('/userProfile/:userId/resetPassword') .patch(controller.resetPassword); - userProfileRouter.route('/userProfile/name/:userId') + userProfileRouter + .route('/userProfile/name/:userId') .get(controller.getUserName); - userProfileRouter.route('/userProfile/project/:projectId') + userProfileRouter + .route('/userProfile/project/:projectId') .get(controller.getProjectMembers); - userProfileRouter.route('/userProfile/socials/facebook') + userProfileRouter + .route('/userProfile/socials/facebook') .get(controller.getAllUsersWithFacebookLink); return userProfileRouter; }; - module.exports = routes; diff --git a/src/routes/wbsRouter.js b/src/routes/wbsRouter.js index b78ada5b9..08bfdc7b5 100644 --- a/src/routes/wbsRouter.js +++ b/src/routes/wbsRouter.js @@ -14,6 +14,9 @@ const routes = function (wbs) { wbsRouter.route('/wbsId/:id') .get(controller.getWBSById); + wbsRouter.route('/wbs/user/:userId') + .get(controller.getWBSByUserId); + wbsRouter.route('/wbs').get(controller.getWBS); return wbsRouter; diff --git a/src/startup/middleware.js b/src/startup/middleware.js index 23ce1d4a6..5f79f77b8 100644 --- a/src/startup/middleware.js +++ b/src/startup/middleware.js @@ -22,7 +22,7 @@ module.exports = function (app) { next(); return; } - if (req.originalUrl === '/api/ProfileInitialSetup' || req.originalUrl === '/api/validateToken' || req.originalUrl === '/api/getTimeZoneAPIKeyByToken' && req.method === 'POST' + if (req.originalUrl === '/api/ProfileInitialSetup' || req.originalUrl === '/api/validateToken' || req.originalUrl === '/api/getTimeZoneAPIKeyByToken' && req.method === 'POST' || req.originalUrl === '/api/getTotalCountryCount' && req.method === 'GET' ) { next(); return; diff --git a/src/startup/routes.js b/src/startup/routes.js index 2fd7337a6..492e455cd 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -7,7 +7,6 @@ const actionItem = require('../models/actionItem'); const notification = require('../models/notification'); const wbs = require('../models/wbs'); const task = require('../models/task'); -const timer = require('../models/timer'); const popup = require('../models/popupEditor'); const popupBackup = require('../models/popupEditorBackup'); const taskNotification = require('../models/taskNotification'); @@ -17,15 +16,32 @@ const inventoryItemType = require('../models/inventoryItemType'); const role = require('../models/role'); const rolePreset = require('../models/rolePreset'); const ownerMessage = require('../models/ownerMessage'); -const ownerStandardMessage = require('../models/ownerStandardMessage'); + +const weeklySummaryAIPrompt = require('../models/weeklySummaryAIPrompt'); const profileInitialSetuptoken = require('../models/profileInitialSetupToken'); const reason = require('../models/reason'); const mouseoverText = require('../models/mouseoverText'); -const inventoryItemMaterial = require('../models/inventoryItemMaterial'); +// const inventoryItemMaterial = require('../models/inventoryItemMaterial'); +const mapLocations = require('../models/mapLocation'); +const buildingProject = require('../models/bmdashboard/buildingProject'); +// const buildingMaterial = require('../models/bmdashboard/buildingMaterial'); +const { + invTypeBase, + materialType, + consumableType, + reusableType, + toolType, + equipmentType, +} = require('../models/bmdashboard/buildingInventoryType'); +const { + buildingConsumable, + buildingMaterial, +} = require('../models/bmdashboard/buildingInventoryItem'); +const buildingTool = require('../models/bmdashboard/buildingTool'); const userProfileRouter = require('../routes/userProfileRouter')(userProfile); const badgeRouter = require('../routes/badgeRouter')(badge); -const dashboardRouter = require('../routes/dashboardRouter')(); +const dashboardRouter = require('../routes/dashboardRouter')(weeklySummaryAIPrompt); const timeEntryRouter = require('../routes/timeentryRouter')(timeEntry); const projectRouter = require('../routes/projectRouter')(project); const informationRouter = require('../routes/informationRouter')(information); @@ -38,13 +54,12 @@ const forcePwdRouter = require('../routes/forcePwdRouter')(userProfile); const reportsRouter = require('../routes/reportsRouter')(); const wbsRouter = require('../routes/wbsRouter')(wbs); const taskRouter = require('../routes/taskRouter')(task); -const timerRouter = require('../routes/timerRouter')(timer); const popupRouter = require('../routes/popupEditorRouter')(popup); const popupBackupRouter = require('../routes/popupEditorBackupRouter')(popupBackup); const taskNotificationRouter = require('../routes/taskNotificationRouter')(taskNotification); const inventoryRouter = require('../routes/inventoryRouter')(inventoryItem, inventoryItemType); const timeZoneAPIRouter = require('../routes/timeZoneAPIRoutes')(); -const profileInitialSetupRouter = require('../routes/profileInitialSetupRouter')(profileInitialSetuptoken, userProfile, project); +const profileInitialSetupRouter = require('../routes/profileInitialSetupRouter')(profileInitialSetuptoken, userProfile, project, mapLocations); const isEmailExistsRouter = require('../routes/isEmailExistsRouter')(); @@ -53,14 +68,19 @@ const taskEditSuggestionRouter = require('../routes/taskEditSuggestionRouter')(t const roleRouter = require('../routes/roleRouter')(role); const rolePresetRouter = require('../routes/rolePresetRouter')(rolePreset); const ownerMessageRouter = require('../routes/ownerMessageRouter')(ownerMessage); -const ownerStandardMessageRouter = require('../routes/ownerStandardMessageRouter')(ownerStandardMessage); const reasonRouter = require('../routes/reasonRouter')(reason, userProfile); const mouseoverTextRouter = require('../routes/mouseoverTextRouter')(mouseoverText); +const mapLocationRouter = require('../routes/mapLocationsRouter')(mapLocations); + // bm dashboard const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')(); -const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(inventoryItemMaterial); +const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(buildingMaterial); +const bmProjectRouter = require('../routes/bmdashboard/bmProjectRouter')(buildingProject); +const bmConsumablesRouter = require('../routes/bmdashboard/bmConsumablesRouter')(buildingConsumable); +const bmInventoryTypeRouter = require('../routes/bmdashboard/bmInventoryTypeRouter')(invTypeBase, materialType, consumableType, reusableType, toolType, equipmentType); +const bmToolRouter = require('../routes/bmdashboard/bmToolRouter')(buildingTool); module.exports = function (app) { app.use('/api', forgotPwdRouter); @@ -76,7 +96,6 @@ module.exports = function (app) { app.use('/api', reportsRouter); app.use('/api', wbsRouter); app.use('/api', taskRouter); - app.use('/api', timerRouter); app.use('/api', popupRouter); app.use('/api', popupBackupRouter); app.use('/api', taskNotificationRouter); @@ -87,13 +106,17 @@ module.exports = function (app) { app.use('/api', roleRouter); app.use('/api', rolePresetRouter); app.use('/api', ownerMessageRouter); - app.use('/api', ownerStandardMessageRouter); app.use('/api', profileInitialSetupRouter); app.use('/api', reasonRouter); app.use('/api', informationRouter); app.use('/api', mouseoverTextRouter); app.use('/api', isEmailExistsRouter); + app.use('/api', mapLocationRouter); // bm dashboard app.use('/api/bm', bmLoginRouter); app.use('/api/bm', bmMaterialsRouter); -}; + app.use('/api/bm', bmProjectRouter); + app.use('/api/bm', bmInventoryTypeRouter); + app.use('/api/bm', bmToolRouter); + app.use('/api/bm', bmConsumablesRouter); +}; \ No newline at end of file diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 9cdf7f9e0..86a79b94c 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -6,6 +6,9 @@ const permissionsRoles = [ { roleName: 'Administrator', permissions: [ + // Reports + 'getWeeklySummaries', + 'getReports', // Doesn't do anything on back-end. // Badges 'seeBadges', 'assignBadges', @@ -65,8 +68,7 @@ const permissionsRoles = [ // General 'getUserProfiles', 'getProjectMembers', - 'getWeeklySummaries', - // 'getReportsPage',? + 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', ], @@ -97,6 +99,7 @@ const permissionsRoles = [ 'getAllInvType', 'postInvType', 'getWeeklySummaries', + 'getReports', 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', ], @@ -124,7 +127,6 @@ const permissionsRoles = [ 'putInvType', 'getAllInvType', 'postInvType', - 'getWeeklySummaries', 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', ], @@ -151,7 +153,6 @@ const permissionsRoles = [ 'putInvType', 'getAllInvType', 'postInvType', - 'getWeeklySummaries', 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', ], @@ -212,6 +213,7 @@ const permissionsRoles = [ 'getAllInvType', 'postInvType', 'getWeeklySummaries', + 'getReports', 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', 'editTeamCode', diff --git a/src/utilities/emailSender.js b/src/utilities/emailSender.js index 4841d1410..7199202e9 100644 --- a/src/utilities/emailSender.js +++ b/src/utilities/emailSender.js @@ -2,7 +2,6 @@ const nodemailer = require('nodemailer'); const { google } = require('googleapis'); const logger = require('../startup/logger'); - const closure = () => { const queue = []; @@ -36,8 +35,8 @@ const closure = () => { if (!nextItem) return; const { - recipient, subject, message, cc, bcc, - } = nextItem; + recipient, subject, message, cc, bcc, replyTo, acknowledgingReceipt, +} = nextItem; try { // Generate the accessToken on the fly @@ -51,6 +50,7 @@ const closure = () => { bcc, subject, html: message, + replyTo, auth: { user: CLIENT_EMAIL, refreshToken: REFRESH_TOKEN, @@ -59,16 +59,36 @@ const closure = () => { }; const result = await transporter.sendMail(mailOptions); + if (typeof acknowledgingReceipt === 'function') { + acknowledgingReceipt(null, result); + } logger.logInfo(result); } catch (error) { + if (typeof acknowledgingReceipt === 'function') { + acknowledgingReceipt(error, null); + } logger.logException(error); } }, process.env.MAIL_QUEUE_INTERVAL || 1000); - const emailSender = function (recipient, subject, message, cc = null, bcc = null) { + const emailSender = function ( + recipient, + subject, + message, + cc = null, + bcc = null, + replyTo = null, + acknowledgingReceipt = null, + ) { if (process.env.sendEmail) { queue.push({ - recipient, subject, message, cc, bcc, + recipient, + subject, + message, + cc, + bcc, + replyTo, + acknowledgingReceipt, }); } }; diff --git a/src/utilities/escapeRegex.js b/src/utilities/escapeRegex.js index 10fa2e61e..cf7563e26 100644 --- a/src/utilities/escapeRegex.js +++ b/src/utilities/escapeRegex.js @@ -1,6 +1,6 @@ const escapeRegex = function (text) { - return text.replace(/[[\]{}()*+?.\\^$|]/g, '\\$&'); + return `^${text.replace(/[[\]{}()*+?.\\^$|]/g, '\\$&')}$`; }; module.exports = escapeRegex; diff --git a/src/utilities/permissions.js b/src/utilities/permissions.js index 7b5d4a245..ebf35b2a1 100644 --- a/src/utilities/permissions.js +++ b/src/utilities/permissions.js @@ -4,12 +4,14 @@ const UserProfile = require('../models/userProfile'); const hasRolePermission = async (role, action) => Role.findOne({ roleName: role }) .exec() - .then(({ permissions }) => permissions.includes(action)); + .then(({ permissions }) => permissions.includes(action)) + .catch(false); const hasIndividualPermission = async (userId, action) => UserProfile.findById(userId) .select('permissions') .exec() - .then(({ permissions }) => permissions.frontPermissions.includes(action)); + .then(({ permissions }) => permissions.frontPermissions.includes(action)) + .catch(false); const hasPermission = async (requestor, action) => await hasRolePermission(requestor.role, action) || hasIndividualPermission(requestor.requestorId, action); diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js new file mode 100644 index 000000000..60eb33fa4 --- /dev/null +++ b/src/websockets/TimerService/clientsHandler.js @@ -0,0 +1,218 @@ +/* eslint-disable no-multi-assign */ +/* eslint-disable radix */ +const moment = require('moment'); +const Timer = require('../../models/timer'); +const logger = require('../../startup/logger'); + +export const getClient = async (clients, userId) => { + // In case of there is already a connection that is open for this user + // for example user open a new connection + if (!clients.has(userId)) { + try { + let timer = await Timer.findOne({ userId }); + if (!timer) timer = await Timer.create({ userId }); + clients.set(userId, timer); + } catch (e) { + logger.logException(e); + throw new Error( + 'Something happened when trying to retrieve timer from mongo', + ); + } + } + return clients.get(userId); +}; + +export const saveClient = async (client) => { + try { + await Timer.findOneAndUpdate({ userId: client.userId }, client); + } catch (e) { + logger.logException(e); + throw new Error( + `Something happened when trying to save user timer to mongo, Error: ${e}`, + ); + } +}; + +export const action = { + START_TIMER: 'START_TIMER', + PAUSE_TIMER: 'PAUSE_TIMER', + STOP_TIMER: 'STOP_TIMER', + CLEAR_TIMER: 'CLEAR_TIMER', + SET_GOAL: 'SET_GOAL=', + ADD_GOAL: 'ADD_TO_GOAL=', + REMOVE_GOAL: 'REMOVE_FROM_GOAL=', + FORCED_PAUSE: 'FORCED_PAUSE', + ACK_FORCED: 'ACK_FORCED', + START_CHIME: 'START_CHIME', +}; + +const MAX_HOURS = 5; +const MIN_MINS = 1; + +const updatedTimeSinceStart = (client) => { + if (!client.started) return client.goal; + const now = moment.utc(); + const startAt = moment(client.startAt); + const timePassed = moment.duration(now.diff(startAt)).asMilliseconds(); + const updatedTime = client.time - timePassed; + return updatedTime > 0 ? updatedTime : 0; +}; + +const startTimer = (client) => { + client.startAt = moment.utc(); + client.paused = false; + if (!client.started) { + client.started = true; + client.time = client.goal; + } + if (client.forcedPause) client.forcedPause = false; +}; + +const pauseTimer = (client, forced = false) => { + client.time = updatedTimeSinceStart(client); + if (client.time === 0) client.chiming = true; + client.startAt = moment.invalid(); // invalid can not be saved in database + client.paused = true; + if (forced) client.forcedPause = true; +}; + +const startChime = (client, msg) => { + const state = msg.split('=')[1]; + client.chiming = state === 'true'; +}; + +const ackForcedPause = (client) => { + client.forcedPause = false; + client.paused = true; + client.startAt = moment.invalid(); +}; + +const stopTimer = (client) => { + if (client.started) pauseTimer(client); + client.startAt = moment.invalid(); + client.started = false; + client.pause = false; + client.forcedPause = false; + if (client.chiming) client.chiming = false; + if (client.time === 0) { + client.goal = client.initialGoal; + client.time = client.goal; + } else { + client.goal = client.time; + } +}; + +const clearTimer = (client) => { + stopTimer(client); + client.goal = client.initialGoal; + client.chiming = false; + client.time = client.goal; +}; + +const setGoal = (client, msg) => { + const newGoal = parseInt(msg.split('=')[1]); + client.goal = newGoal; + client.time = newGoal; + client.initialGoal = newGoal; +}; + +const addGoal = (client, msg) => { + const duration = parseInt(msg.split('=')[1]); + const goalAfterAddition = moment + .duration(client.goal) + .add(duration, 'milliseconds') + .asHours(); + + if (goalAfterAddition > MAX_HOURS) return; + + client.goal = moment + .duration(client.goal) + .add(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); + client.time = moment + .duration(client.time) + .add(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); +}; + +const removeGoal = (client, msg) => { + const duration = parseInt(msg.split('=')[1]); + const goalAfterRemoval = moment + .duration(client.goal) + .subtract(duration, 'milliseconds') + .asMinutes(); + const timeAfterRemoval = moment + .duration(client.time) + .subtract(duration, 'milliseconds') + .asMinutes(); + + if (goalAfterRemoval < MIN_MINS || timeAfterRemoval < 0) return; + + client.goal = moment + .duration(client.goal) + .subtract(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); + client.time = moment + .duration(client.time) + .subtract(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); +}; + +export const handleMessage = async (msg, clients, userId) => { + if (!clients.has(userId)) { + throw new Error('It should have this user in memory'); + } + + const client = clients.get(userId); + let resp = null; + + const req = msg.toString(); + switch (req) { + case action.START_TIMER: + startTimer(client); + break; + case req.match(/SET_GOAL=/i)?.input: + setGoal(client, req); + break; + case req.match(/ADD_TO_GOAL=/i)?.input: + addGoal(client, req); + break; + case req.match(/REMOVE_FROM_GOAL=/i)?.input: + removeGoal(client, req); + break; + case req.match(/START_CHIME=/i)?.input: + startChime(client, req); + break; + case action.PAUSE_TIMER: + pauseTimer(client); + break; + case action.FORCED_PAUSE: + pauseTimer(client, true); + break; + case action.ACK_FORCED: + ackForcedPause(client); + break; + case action.CLEAR_TIMER: + clearTimer(client); + break; + case action.STOP_TIMER: + stopTimer(client); + break; + + default: + resp = { + ...client, + error: `Unknown operation ${req}, please use one of ${action}`, + }; + break; + } + + saveClient(client); + clients.set(userId, client); + if (resp === null) resp = client; + return JSON.stringify(resp); +}; diff --git a/src/websockets/TimerService/connectionsHandler.js b/src/websockets/TimerService/connectionsHandler.js new file mode 100644 index 000000000..6658321bf --- /dev/null +++ b/src/websockets/TimerService/connectionsHandler.js @@ -0,0 +1,50 @@ +const WebSocket = require('ws'); + +/** + * Here we insert the new connection to the connections map. + * If the user is not in the map, we create a new entry with the user id as key and the connection as value. + * Else we just push the connection to the array of connections. + */ +export function insertNewUser(connections, userId, wsConn) { + const userConnetions = connections.get(userId); + if (!userConnetions) connections.set(userId, [wsConn]); + else userConnetions.push(wsConn); +} + +/** + *Here we remove the connection from the connections map. + *If the user is not in the map, we do nothing. + *Else we remove the connection from the array of connections. + *If the array is empty, we delete the user from the map. + */ +export function removeConnection(connections, userId, connToRemove) { + const userConnetions = connections.get(userId); + if (!userConnetions) return; + + const newConns = userConnetions.filter(conn => conn !== connToRemove); + if (newConns.length === 0) connections.delete(userId); + else connections.set(userId, newConns); +} + +/** + * Here we broadcast the message to all the connections that are connected to the same user. + * We check if the connection is open before sending the message. + */ +export function broadcastToSameUser(connections, userId, data) { + const userConnetions = connections.get(userId); + if (!userConnetions) return; + userConnetions.forEach((conn) => { + if (conn.readyState === WebSocket.OPEN) conn.send(data); + }); +} + +/** + * Here we check if there is another connection to the same user. + * If there is, we return true. + * Else we return false. + */ +export function hasOtherConn(connections, userId, anotherConn) { + if (!connections.has(userId)) return false; + const userConnections = connections.get(userId); + return userConnections.some(con => con !== anotherConn && con.readyState === WebSocket.OPEN); +} diff --git a/src/websockets/TimerService/index.js b/src/websockets/TimerService/index.js deleted file mode 100644 index 9eac199ce..000000000 --- a/src/websockets/TimerService/index.js +++ /dev/null @@ -1,308 +0,0 @@ -/* eslint-disable no-multi-assign */ -/* eslint-disable radix */ -const moment = require('moment'); -const Timer = require('../../models/timer'); -const logger = require('../../startup/logger'); - -/* -This is the contract between client and server. -The client can send one of the following messages to the server: -*/ -export const action = { - START_TIMER: 'START_TIMER', - PAUSE_TIMER: 'PAUSE_TIMER', - STOP_TIMER: 'STOP_TIMER', - GET_TIMER: 'GET_TIMER', - CLEAR_TIMER: 'CLEAR_TIMER', - SWITCH_MODE: 'SWITCH_MODE', - SET_GOAL: 'SET_GOAL=', - ADD_GOAL: 'ADD_GOAL=', - REMOVE_GOAL: 'REMOVE_GOAL=', - FORCED_PAUSE: 'FORCED_PAUSE', - ACK_FORCED: 'ACK_FORCED', -}; - -/* -Here we get the total elapsed time since the last access. -Since we have two modes for the timer, countdown and stopwatch, -we need to know which one is active to calculate the total elapsed time. -If the timer is in countdown mode, we need to subtract the elapsed time from the total time. -if this total time is less than 0, we set it to 0. -If the timer is in stopwatch mode, -we need to add the elapsed time since the last access to the total time. -we then return the total -*/ -const getTotalElapsedTime = (client) => { - const now = moment(); - const lastAccess = moment(client.lastAccess); - const elapSinceLastAccess = moment.duration(now.diff(lastAccess)); - const time = moment.duration(moment(client.time)); - - let total; - if (client.countdown) { - total = time.subtract(elapSinceLastAccess, 'milliseconds'); - if (total.asMilliseconds() < 0) { - total = moment.duration(0); - } - } else total = elapSinceLastAccess.add(client.time, 'milliseconds'); - - return total; -}; - -/* -Here we start the timer, if it is not already started. -We set the last access time to now, and set the paused and stopped flags to false. -If the timer was paused, we need to check if it was paused by the user or by the server. -If it was paused by the server, we need to set the forcedPause flag to true. -*/ -const startTimer = (client) => { - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - } - - if (client.paused) { - client.lastAccess = moment(); - client.stopped = false; - client.paused = false; - if (client.forcedPause) client.forcedPause = false; - } -}; - -/* -Here we pause the timer, if it is not already paused. -We get the total elapsed time since the last access, and set it as the new time. -We set the last access time to now, and set the paused flag to true. -If the timer was paused by the server, we need to set the forcedPause flag to true. -It'll only be triggered when the user closes the connection sudenlly or lacks of ACKs. -*/ -const pauseTimer = (client, forced = false) => { - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - client.paused = true; - if (forced) client.forcedPause = true; - } -}; - -// Here we acknowledge the forced pause. To prevent the modal for beeing displayed again. -const ackForcedPause = (client) => { - client.forcedPause = false; -}; - -/* -Here we clear the timer. -We pause the timer and check it's mode to set the time to 0 or the goal. -Then we set the stopped flag to false. -*/ -const clearTimer = (client) => { - pauseTimer(client); - client.time = client.countdown ? client.goal : 0; - client.stopped = false; -}; - -/* -Here we stop the timer. -We pause the timer and set the stopped flag to true. -*/ -const stopTimer = (client) => { - pauseTimer(client); - client.stopped = true; -}; - -/* -Here we switch the timer mode. -We pause the timer and check it's mode to set the time to 0 or the goal. -*/ -const switchMode = (client) => { - client.countdown = !client.countdown; - client.time = client.countdown ? client.goal : 0; - client.paused = true; -}; - -// Here we get the goal time from the message. -const getGoal = msg => parseInt(msg.split('=')[1]); - -// Here we set the goal and time to the goal time. -const setGoal = (client, msg) => { - const goal = getGoal(msg); - client.goal = client.time = goal; -}; - -const goalOver10Hours = (client, time) => { - const goal = moment.duration(client.goal).add(time, 'milliseconds').asHours(); - return goal > 10; -}; - -/* -Here we add the goal time. -First we get the goal time from the message. -Then we add it to the current goal time and set it as the new goal time. -We also add it to the current time and set it as the new time. -*/ -const addGoal = (client, msg) => { - const goal = getGoal(msg); - if (goalOver10Hours(client, goal)) return; - - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - } - - client.goal = moment - .duration(client.goal) - .add(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); - client.time = moment - .duration(client.time) - .add(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); -}; - -/* - * Here we check if the goal time is less than 15 minutes. - * */ -const goalLessThan15min = (client, time) => { - const goal = moment - .duration(client.goal) - .subtract(time, 'milliseconds') - .asMinutes(); - return goal < 15; -}; - -/* - * Here we try to remove a goal time. - * First we get the goal time from the message. - * Then we subtract it from the current goal time and set it as the new goal time. - * We also subtract it from the current time and set it as the new time. - * If the new goal time is less than 15 minutes, we don't do anything. - * If the new time is less than 0, we set it to 0. - * */ -const removeGoal = (client, msg) => { - const goal = getGoal(msg); - if (goalLessThan15min(client, goal)) return; - - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - } - - client.goal = moment - .duration(client.goal) - .subtract(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); - const time = moment - .duration(client.time) - .subtract(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); - client.time = time < 0 ? 0 : time; -}; - -/* -Here we get the timer. -If the timer already exists in memory, we return it. -If it doesn't exist, we try to get it from MongoDB. -If it doesn't exist in MongoDB, we create it and save it to MongoDB. -Then we save it to memory and return it. -*/ -export const getTimer = async (clientsMap, userId) => { - if (clientsMap.has(userId)) return; - - try { - let timer = await Timer.findOne({ userId }); - if (!timer) timer = await Timer.create({ userId }); - clientsMap.set(userId, timer); - } catch (e) { - logger.logException(e); - throw new Error( - 'Something happened when trying to retrieve timer from mongo', - ); - } -}; - -// Here we just save the timer to MongoDB. -const saveClient = async (client) => { - try { - await Timer.findOneAndUpdate({ userId: client.userId }, client); - } catch (e) { - logger.logException(e); - throw new Error( - 'Something happened when trying to save user timer to mongo', - ); - } -}; - -/* -Here is were we handle the messages. -First we check if the user is in memory, if not, we throw an error. -Then we parse the request and check which action it is and call the corresponding function. -If we don't have a match, we just return an error. -The only operation that we write to Mongo it's the stop timer. Other operations are just in memory. -So the slowest part of the app is the save to Mongo. -Then we update the current client in hash map and return the response. -*/ -export const handleMessage = async (msg, clientsMap, userId) => { - if (!clientsMap.has(userId)) { - throw new Error('It should have this user in memory'); - } - - const client = clientsMap.get(userId); - let resp = null; - - const req = msg.toString(); - switch (req) { - case action.GET_TIMER: - break; - case action.START_TIMER: - startTimer(client); - break; - case action.SWITCH_MODE: - switchMode(client); - break; - case req.match(/SET_GOAL=/i)?.input: - setGoal(client, req); - break; - case req.match(/ADD_GOAL=/i)?.input: - addGoal(client, req); - break; - case req.match(/REMOVE_GOAL=/i)?.input: - removeGoal(client, req); - break; - case action.PAUSE_TIMER: - pauseTimer(client); - break; - case action.FORCED_PAUSE: - pauseTimer(client, true); - break; - case action.ACK_FORCED: - ackForcedPause(client); - break; - case action.CLEAR_TIMER: - clearTimer(client); - break; - case action.STOP_TIMER: - stopTimer(client); - break; - - default: - resp = { - ...client, - error: `Unknown operation ${req}, please use one of ${action}`, - }; - break; - } - - if (req === action.STOP_TIMER) { - await saveClient(client).catch((err) => { - resp = { ...client, error: err }; - }); - } - - clientsMap.set(userId, client); - if (resp === null) resp = client; - return JSON.stringify(resp); -}; diff --git a/src/websockets/index.js b/src/websockets/index.js index a733dff25..5da85f729 100644 --- a/src/websockets/index.js +++ b/src/websockets/index.js @@ -7,15 +7,25 @@ const WebSocket = require("ws"); const moment = require("moment"); const jwt = require("jsonwebtoken"); const config = require("../config"); -const { getTimer, handleMessage, action } = require("./TimerService/"); - -/* -Here we authenticate the user. -We get the token from the headers and try to verify it. -If it fails, we throw an error. -Else we check if the token is valid and if it is, we return the user id. +const { + insertNewUser, + removeConnection, + broadcastToSameUser, + hasOtherConn, +} = require("./TimerService/connectionsHandler"); +const { + getClient, + handleMessage, + action, +} = require("./TimerService/clientsHandler"); + +/** +* Here we authenticate the user. +* We get the token from the headers and try to verify it. +* If it fails, we throw an error. +* Else we check if the token is valid and if it is, we return the user id. */ -export const authenticate = (req, res) => { +const authenticate = (req, res) => { const authToken = req.headers?.["sec-websocket-protocol"]; let payload = ""; try { @@ -37,68 +47,13 @@ export const authenticate = (req, res) => { res(null, payload.userid); }; -/* - * Here we insert the new connection to the connections map. - * If the user is not in the map, we create a new entry with the user id as key and the connection as value. - * Else we just push the connection to the array of connections. - */ -const insertNewUser = (connections, userId, wsConn) => { - const userConnetions = connections.get(userId); - if (!userConnetions) connections.set(userId, [wsConn]); - else userConnetions.push(wsConn); -}; - -/* - *Here we remove the connection from the connections map. - *If the user is not in the map, we do nothing. - *Else we remove the connection from the array of connections. - *If the array is empty, we delete the user from the map. - */ -const removeConnection = (connections, userId, connToRemove) => { - const userConnetions = connections.get(userId); - if (!userConnetions) return; - - const newConns = userConnetions.filter(conn => conn !== connToRemove); - if (newConns.length === 0) connections.delete(userId); - else connections.set(userId, newConns); -}; - -/* - * Here we broadcast the message to all the connections that are connected to the same user. - * We check if the connection is open before sending the message. - */ -const broadcastToSameUser = (connections, userId, data) => { - const userConnetions = connections.get(userId); - if (!userConnetions) return; - userConnetions.forEach((conn) => { - if (conn.readyState === WebSocket.OPEN) conn.send(data); - }); -}; - -/* - * Here we check if there is another connection to the same user. - * If there is, we return true. - * Else we return false. - */ -const checkOtherConn = (connections, anotherConn, userId) => { - const userConnetions = connections.get(userId); - if (!userConnetions) return false; - for (const con of userConnetions) { - if (con !== anotherConn && con.readyState === WebSocket.OPEN) return true; - } - return false; -}; - -/* -Here we start the timer service. -First we create a map to store the clients and start the Websockets Server. -Then we set the upgrade event listener to the Express Server, authenticate the user and -if it is valid, we add the user id to the request and handle the upgrade and emit the connection event. +/** +* Here we start the timer service. +* First we create a map to store the clients and start the Websockets Server. +* Then we set the upgrade event listener to the Express Server, authenticate the user and +* if it is valid, we add the user id to the request and handle the upgrade and emit the connection event. */ export default async (expServer) => { - const clients = new Map(); - const connections = new Map(); - const wss = new WebSocket.Server({ noServer: true, path: "/timer-service", @@ -118,64 +73,66 @@ export default async (expServer) => { }); }); - /* - For each new connection we start a timer of 5min to check if the connection is alive. - If it is, we then repeat the process. If it is not, we terminate the connection. - */ + const clients = new Map(); // { userId: timerInfo } + const connections = new Map(); // { userId: connections[] } + wss.on("connection", async (ws, req) => { ws.isAlive = true; + const { userId } = req; + ws.on("pong", () => { ws.isAlive = true; }); - const { userId } = req; - insertNewUser(connections, userId, ws); - /* + /** * Here we get the timer from memory or from the database and send it to the client. - * We don't broadcast it */ - await getTimer(clients, userId); - ws.send(await handleMessage(action.GET_TIMER, clients, userId)); + const clientTimer = await getClient(clients, userId); + ws.send(JSON.stringify(clientTimer)); - /* - Here we handle the messages from the client. - And we broadcast the response to all the clients that are connected to the same user. + /** + * Here we handle the messages from the client. + * And we broadcast the response to all the clients that are connected to the same user. */ ws.on("message", async (data) => { const resp = await handleMessage(data, clients, userId); broadcastToSameUser(connections, userId, resp); }); - /* - Here we handle the close event. - If there is another connection to the same user, we don't do anything. - Else he is the last connection and we do a forced pause if need be. - This may happen if the user closes all the tabs or the browser or he lost connection with - the service - We then remove the connection from the connections map. + /** + * Here we handle the close event. + * If there is another connection to the same user, we don't do anything. + * Else he is the last connection and we do a forced pause if need be. + * This may happen if the user closes all the tabs or the browser or he lost connection with + * the service + * We then remove the connection from the connections map. */ ws.on("close", async () => { - if (!checkOtherConn(connections, ws, userId)) { - await handleMessage(action.FORCED_PAUSE, clients, userId); + if (!hasOtherConn(connections, userId, ws)) { + const client = clients.get(userId); + if (client.started && !client.paused) { + await handleMessage(action.FORCED_PAUSE, clients, userId); + } } removeConnection(connections, userId, ws); }); }); - // The function to check if the connection is alive - const interval = setInterval(async () => { - wss.clients.forEach(async (ws) => { - if (ws.isAlive === false) return ws.terminate(); - + // For each new connection we start a time interval of 1min to check if the connection is alive. + // change to 1min before push + const interval = setInterval(() => { + wss.clients.forEach((ws) => { + if (ws.isAlive === false) { + return ws.close(); + } ws.isAlive = false; ws.ping(); }); - }, 3000000); + }, 10000); - // Here we just clear the interval when the server closes - wss.on("close", () => { + wss.on('close', () => { clearInterval(interval); });