From 6adc030a4c491c32876f4e72a6230e99763dd1f4 Mon Sep 17 00:00:00 2001 From: circlegov Date: Sun, 28 Jan 2024 15:19:28 -0500 Subject: [PATCH 01/14] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b04ae70..014d82f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,13 @@ ## Start the app -To start the development server run `nx serve org`. Open your browser and navigate to http://localhost:4200/. Happy coding! +run `npm i` - will install all dependencies + +to run both backend and frontend at the same time run: `npx nx run-many -t start` +to start them individually run: `npx nx start backend` or npx nx start frontend` + +to run prettier and check all files run: `npx nx format:check --all` +to run prettier and format all files run: `npx nx format:write --all` ## Generate code From c24dbd904595b47426137c8b70fa2946c93b02f1 Mon Sep 17 00:00:00 2001 From: circlegov Date: Sun, 28 Jan 2024 15:19:50 -0500 Subject: [PATCH 02/14] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 014d82f..52a72c0 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,14 @@ run `npm i` - will install all dependencies + to run both backend and frontend at the same time run: `npx nx run-many -t start` + to start them individually run: `npx nx start backend` or npx nx start frontend` + to run prettier and check all files run: `npx nx format:check --all` + to run prettier and format all files run: `npx nx format:write --all` From 79b00fa109c94f188bba817c7912284a3f4591a0 Mon Sep 17 00:00:00 2001 From: David Levin Date: Sun, 4 Feb 2024 14:44:54 -0500 Subject: [PATCH 03/14] added precommit hook for prettier with lint-staged --- .husky/.gitignore | 1 + .husky/pre-commit | 4 + package-lock.json | 651 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 8 +- 4 files changed, 663 insertions(+), 1 deletion(-) create mode 100644 .husky/.gitignore create mode 100755 .husky/pre-commit diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..36af219 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/package-lock.json b/package-lock.json index 272405e..7923253 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,9 @@ "eslint-plugin-playwright": "^0.15.3", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", + "husky": "^9.0.10", "jsdom": "~22.1.0", + "lint-staged": "^15.2.1", "npm-run-all": "^4.1.5", "nx": "17.2.8", "prettier": "^2.6.2", @@ -5246,6 +5248,33 @@ "node": ">=6" } }, + "node_modules/ansi-escapes": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "dev": true, + "dependencies": { + "type-fest": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -6024,6 +6053,72 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -8024,6 +8119,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -8452,6 +8559,21 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.10.tgz", + "integrity": "sha512-TQGNknoiy6bURzIO77pPRu+XHi6zI7T93rX+QnJsoYFf3xdjKOur+IlfqzJGMHIK/wXrLg+GsvMs8Op7vI2jVA==", + "dev": true, + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -9271,6 +9393,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/lines-and-columns": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", @@ -9280,6 +9411,311 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/lint-staged": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.1.tgz", + "integrity": "sha512-dhwAPnM85VdshybV9FWI/9ghTvMLoQLEXgVMx+ua2DN7mdfzd/tRfoU2yhMcBac0RHkofoxdnnJUokr8s4zKmQ==", + "dev": true, + "dependencies": { + "chalk": "5.3.0", + "commander": "11.1.0", + "debug": "4.3.4", + "execa": "8.0.1", + "lilconfig": "3.0.0", + "listr2": "8.0.1", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/lint-staged/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/listr2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz", + "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.0.0", + "rfdc": "^1.3.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -9402,6 +9838,166 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", + "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^6.2.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^7.0.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -11000,6 +11596,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -11346,6 +11948,46 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -11499,6 +12141,15 @@ } ] }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", diff --git a/package.json b/package.json index 46c5a42..5776940 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "postinstall": "run-p install:*", "frontend": "nx start frontend", "backend": "nx start:dev backend", - "start": "npm-run-all --parallel frontend backend" + "start": "npm-run-all --parallel frontend backend", + "prepare": "husky install" }, "private": true, "dependencies": { @@ -46,12 +47,17 @@ "eslint-plugin-playwright": "^0.15.3", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", + "husky": "^9.0.10", "jsdom": "~22.1.0", + "lint-staged": "^15.2.1", "npm-run-all": "^4.1.5", "nx": "17.2.8", "prettier": "^2.6.2", "typescript": "~5.2.2", "vite": "^5.0.0", "vitest": "~0.34.6" + }, + "lint-staged": { + "*": "npx nx format:write --files" } } From 7f4f45e7200ce66f6bd6218844f8d35738b97b58 Mon Sep 17 00:00:00 2001 From: circlegov Date: Sun, 4 Feb 2024 15:00:28 -0500 Subject: [PATCH 04/14] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 52a72c0..16ea193 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ run `npm i` - will install all dependencies +for the backend you must add an .env, reach out to tech leads/any one on the team for it. + to run both backend and frontend at the same time run: `npx nx run-many -t start` @@ -20,6 +22,8 @@ to run prettier and check all files run: `npx nx format:check --all` to run prettier and format all files run: `npx nx format:write --all` + + ## Generate code If you happen to use Nx plugins, you can leverage code generators that might come with it. From 8c6c2eaaff87496f27107519f77326fd5d565b3c Mon Sep 17 00:00:00 2001 From: izzyconner <32255130+izzyconner@users.noreply.github.com> Date: Sun, 11 Feb 2024 11:44:33 -0500 Subject: [PATCH 05/14] Implemented submit card hookup and global user context Co-authored-by: Rachel Ma --- .../src/components/Auth/UserUtils.tsx | 51 ++ .../src/components/Auth/apiClient.tsx | 22 +- .../components/TimeCardPage/SubmitCard.tsx | 237 ++++-- .../src/components/TimeCardPage/TimeSheet.tsx | 690 ++++++++++-------- .../src/components/TimeCardPage/types.tsx | 67 +- apps/frontend/src/schemas/StatusSchema.tsx | 21 + apps/frontend/src/schemas/TimesheetSchema.tsx | 40 +- apps/frontend/src/schemas/UserSchema.tsx | 3 +- .../frontend/src/schemas/backend/Timesheet.ts | 20 +- .../src/schemas/backend/UpdateTimesheet.ts | 60 +- 10 files changed, 787 insertions(+), 424 deletions(-) create mode 100644 apps/frontend/src/components/Auth/UserUtils.tsx create mode 100644 apps/frontend/src/schemas/StatusSchema.tsx diff --git a/apps/frontend/src/components/Auth/UserUtils.tsx b/apps/frontend/src/components/Auth/UserUtils.tsx new file mode 100644 index 0000000..b15d694 --- /dev/null +++ b/apps/frontend/src/components/Auth/UserUtils.tsx @@ -0,0 +1,51 @@ +import { Auth } from "aws-amplify"; +import { UserSchema } from "src/schemas/UserSchema"; + +// TODO: Below code is primarily pulled from backend files (User.client.ts and user.service.ts). This should be moved to a shared folder +import { z } from "zod"; +import { UserTypes } from "../TimeCardPage/types"; + +/** + * The client schema of a Cognito attribute. + * e.g : {Name: 'sub', 'value': 'aeddc72a-fe42b78a8-....'} + */ +export const CognitoAttributes = z.object({ + email: z.string(), + given_name: z.string(), + family_name: z.string(), + sub: z.string(), +}); + +/** + * Represents the client schema of a User object returned from Cognito. + */ +export const CognitoUser = z.object({ + attributes: CognitoAttributes, // TODO : should likely expand this out to include all the known attributes we expect from cognito + username: z.string(), +}); + +export type CognitoUser = z.infer; + +const convertCognitoUser = function (user: CognitoUser): UserSchema { + var sub = user.attributes.sub; + var firstName = user.attributes.given_name; + var lastName = user.attributes.family_name; + + // TODO: The type here shouldn't be hard-coded + + return { + FirstName: firstName, + LastName: lastName, + UserID: sub, + Type: UserTypes.Supervisor, + }; +}; + +// TODO: Should this be in a separate file because +export const getCurrentUser = async function (): Promise { + const cognitoCurrentUser = await Auth.currentUserInfo(); + console.log(cognitoCurrentUser); + const currUser = CognitoUser.parse(cognitoCurrentUser); + //TODO: Also make a call to apiClient.getUser() to get the group for this user + return convertCognitoUser(currUser); +}; diff --git a/apps/frontend/src/components/Auth/apiClient.tsx b/apps/frontend/src/components/Auth/apiClient.tsx index 83cc88a..e595ebf 100644 --- a/apps/frontend/src/components/Auth/apiClient.tsx +++ b/apps/frontend/src/components/Auth/apiClient.tsx @@ -2,7 +2,7 @@ import { Auth } from "aws-amplify"; import axios, { AxiosInstance } from "axios"; import { TimeSheetSchema } from "../../schemas/TimesheetSchema"; import { UserSchema } from "../../schemas/UserSchema"; -import { ReportOptions } from "../TimeCardPage/types"; +import { ReportOptions, UserTypes } from "../TimeCardPage/types"; const defaultBaseUrl = process.env.REACT_APP_API_BASE_URL ?? "http://localhost:3000"; @@ -67,7 +67,7 @@ export class ApiClient { } public async updateTimesheet(req): Promise { - return this.axiosInstance.post('/auth/timesheet', req) + return this.axiosInstance.post("/auth/timesheet", req); } // TODO: setup endpoint for associate/supervisor/admin so it returns a list of timesheets for given uuid @@ -93,8 +93,9 @@ export class ApiClient { UserID: "abc", FirstName: "john", LastName: "doe", - Type: "Admin", - Picture: "https://imgs.search.brave.com/DZmzoTAPlNT9HUb2ISfyTd_sPZab1hG4VcyupoK2gwE/rs:fit:860:0:0/g:ce/aHR0cHM6Ly90My5m/dGNkbi5uZXQvanBn/LzAwLzYxLzU0LzA4/LzM2MF9GXzYxNTQw/ODU1X3lFYmIwTlRr/d3ZJVzdaZG1KeThM/aHU1WHJPMXlweURl/LmpwZw", + Type: UserTypes.Admin, + Picture: + "https://imgs.search.brave.com/DZmzoTAPlNT9HUb2ISfyTd_sPZab1hG4VcyupoK2gwE/rs:fit:860:0:0/g:ce/aHR0cHM6Ly90My5m/dGNkbi5uZXQvanBn/LzAwLzYxLzU0LzA4/LzM2MF9GXzYxNTQw/ODU1X3lFYmIwTlRr/d3ZJVzdaZG1KeThM/aHU1WHJPMXlweURl/LmpwZw", }; } @@ -105,21 +106,26 @@ export class ApiClient { UserID: "bcd", FirstName: "joe", LastName: "jane", - Type: "Associate", + Type: UserTypes.Associate, Picture: "https://www.google.com/panda.png", }, ]; } //TODO: hook up to backend - public async saveComment(comment: string, timesheetID: number): Promise { + public async saveComment( + comment: string, + timesheetID: number + ): Promise { return true; } //TODO: hook up to backend - public async saveReport(report: ReportOptions, timesheetID: number): Promise { + public async saveReport( + report: ReportOptions, + timesheetID: number + ): Promise { return true; } - } export default new ApiClient(); diff --git a/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx b/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx index 8d44146..5be3796 100644 --- a/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx +++ b/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx @@ -1,61 +1,216 @@ -import React, { useState, useEffect } from 'react' -import { WeeklyCommentModal } from './CommentModal'; -import { Box, Card, CardHeader, CardBody, CardFooter, Button } from '@chakra-ui/react'; -import { CardState } from './types' -import { CommentSchema } from 'src/schemas/RowSchema'; - -interface SubmitCardProps { - setWeeklyComments: Function; - setWeeklyReports: Function; - weeklyComments: CommentSchema[]; - weeklyReports: CommentSchema[]; +import { CardState } from "./types"; + +import React, { useState, useEffect, useContext } from "react"; +import { Box, Card, CardBody, CardFooter, Button } from "@chakra-ui/react"; +import { DEFAULT_COLORS } from "src/constants"; +import ApiClient from "src/components/Auth/apiClient"; +import * as updateSchemas from "src/schemas/backend/UpdateTimesheet"; +import { StatusType, StatusEntryType } from "src/schemas/StatusSchema"; +import { UserTypes } from "./types"; +import moment from "moment"; +import { useToast } from "@chakra-ui/react"; +import { UserContext } from "./UserContext"; +import { TimesheetStatus } from "src/schemas/TimesheetSchema"; + +interface submitCardProps { + timesheetId: number; + associateId: string; + timesheetStatus: StatusType; + refreshTimesheetCallback: Function; } -export default function SubmitCard({ - setWeeklyComments, - setWeeklyReports, - weeklyComments, - weeklyReports -}: SubmitCardProps) { +export default function SubmitCard(props: submitCardProps) { + const toast = useToast(); + const currUser = useContext(UserContext); + /** Whether or not the logged-in user has submitted this timesheet yet.*/ const [submitted, setSubmitted] = useState(false); + + /** The date and time (as a moment) that the logged-in user submitted/reviewed/finalized this timesheet.*/ const [submitDate, setSubmitDate] = useState(null); + + /** + * The card state which corresponds to the latest status update from the timesheet. Corresponds to card color. + * Note that this is *not* dependent on the logged in user. I.e. if the latest status update was that + * the supervisor had submitted their timesheet review, the card state would be CardState.InReviewAdmin for + * any associate, supervisor, or admin that was viewing the timesheet. + */ const [state, setState] = useState(CardState.Unsubmitted); + // Run whenever there's an update to the current logged in user (i.e. re-render the correct submission status for the user type) useEffect(() => { - //TODO - API Call to determine if the table has been submitted or not. - //Will set submitted? here and also submitDate if it was submitted to grab the date - }, []) - - const submitAction = () => { - setSubmitted(!submitted); - const currentTime = new Date(); - setSubmitDate(currentTime.toString()); - if (state === CardState.Unsubmitted) { - setState(CardState.InReviewEmployer); + if (currUser === undefined) { + return; } - else { + + let statusEntry: StatusEntryType = undefined; + + // Determine the appropriate status entry to match up with the logged in user's role + switch (currUser.Type) { + case UserTypes.Associate: + statusEntry = props.timesheetStatus.HoursSubmitted; + break; + + case UserTypes.Supervisor: + statusEntry = props.timesheetStatus.HoursReviewed; + break; + + case UserTypes.Admin: + statusEntry = props.timesheetStatus.Finalized; + break; + } + + const isSubmitted = statusEntry !== undefined; + setSubmitted(isSubmitted); + + // Set the submitted date to when the corresponding status entry was logged for the user type + if (isSubmitted && statusEntry.Date !== undefined) { + setSubmitDate(moment.unix(statusEntry.Date)); + } + + // Determine the latest status update to set the card state + if (props.timesheetStatus.Finalized !== undefined) { + setState(CardState.AdminFinalized); + } else if (props.timesheetStatus.HoursReviewed !== undefined) { + setState(CardState.InReviewAdmin); + } else if (props.timesheetStatus.HoursSubmitted !== undefined) { + setState(CardState.InReviewSupervisor); + } else { setState(CardState.Unsubmitted); } - } + }, [currUser, props.timesheetStatus]); + + const submitAction = async () => { + console.log("Current user id:", currUser.UserID); + let statusSubmissionType: string; + + // Determine the appropriate status entry to match up with the logged in user's role + switch (currUser.Type) { + case UserTypes.Associate: + statusSubmissionType = TimesheetStatus.HOURS_SUBMITTED; + break; + + case UserTypes.Supervisor: + statusSubmissionType = TimesheetStatus.HOURS_REVIEWED; + break; + + case UserTypes.Admin: + statusSubmissionType = TimesheetStatus.HOURS_SUBMITTED; + break; + } + + // Update the current timesheet to be submitted by the logged-in user. + // The type of status can be determined on the backend by the user type + try { + const response = await ApiClient.updateTimesheet( + updateSchemas.TimesheetUpdateRequest.parse({ + TimesheetID: props.timesheetId, + Operation: updateSchemas.TimesheetOperations.STATUS_CHANGE, + Payload: updateSchemas.StatusChangeRequest.parse({ + TimesheetId: props.timesheetId, + AssociateId: props.associateId, + authorId: currUser.UserID, // TODO: Implement authorId functionality instead of dummy data + statusType: statusSubmissionType, + dateSubmitted: moment().unix(), + }), + }) + ); + + // TODO: Confirm successful 2xx code responSse from API + props.refreshTimesheetCallback(); + + toast({ + title: "Successful submission!", + status: "success", + duration: 3000, + isClosable: true, + }); + } catch (err) { + console.log(err); + toast({ + title: "Uh oh, something went wrong...", + status: "error", + duration: 3000, + isClosable: true, + }); + return; + } + }; return ( - + + className="mb-2 text-center" + > - + - {submitted && - {submitDate} - {state} - - } + {submitted && ( + + {/* TODO: The AuthorIDs below should all be replaced with calls to the API and then have a User profile card there instead (or at least the name, rather than ID lol) */} +
+ {props.timesheetStatus.HoursSubmitted && + props.timesheetStatus.HoursSubmitted.Date ? ( +

+ Associate: {props.timesheetStatus.HoursSubmitted.AuthorID},{" "} +
+ Submitted on:{" "} + {customDateFormat(props.timesheetStatus.HoursSubmitted.Date)} +

+ ) : ( +

Associate: Unsubmitted

+ )} + + {props.timesheetStatus.HoursReviewed && + props.timesheetStatus.HoursReviewed.Date ? ( +

+ Supervsior: {props.timesheetStatus.HoursReviewed.AuthorID},{" "} +
+ Submitted on:{" "} + {customDateFormat(props.timesheetStatus.HoursReviewed.Date)} +

+ ) : ( +

Supervisor: Unsubmitted

+ )} + + {props.timesheetStatus.Finalized && + props.timesheetStatus.Finalized.Date ? ( +

+ Admin: {props.timesheetStatus.Finalized.AuthorID},
+ Submitted on:{" "} + {customDateFormat(props.timesheetStatus.Finalized.Date)} +

+ ) : ( +

Admin: Unsubmitted

+ )} +
+
+ )}
-
+
); +} -} \ No newline at end of file +function customDateFormat(date) { + const dateX = new Date(date * 1000); + return dateX.toLocaleDateString("en-US", { + year: "2-digit", + month: "2-digit", + day: "2-digit", + }); +} diff --git a/apps/frontend/src/components/TimeCardPage/TimeSheet.tsx b/apps/frontend/src/components/TimeCardPage/TimeSheet.tsx index fb0edec..807f0ae 100644 --- a/apps/frontend/src/components/TimeCardPage/TimeSheet.tsx +++ b/apps/frontend/src/components/TimeCardPage/TimeSheet.tsx @@ -1,341 +1,415 @@ -import React, {useState, useMemo} from 'react'; -import TimeTable from './TimeTable' -import {useEffect} from 'react'; -import SubmitCard from './SubmitCard'; -import DateSelectorCard from './SelectWeekCard'; -import {UserContext} from './UserContext'; +import React, { useState, useMemo } from "react"; +import TimeTable from "./TimeTable"; +import { useEffect } from "react"; +import SubmitCard from "./SubmitCard"; +import DateSelectorCard from "./SelectWeekCard"; +import { UserContext } from "./UserContext"; +import { getCurrentUser } from "../Auth/UserUtils"; import { - Alert, - AlertIcon, - AlertTitle, - AlertDescription, - Box, - IconButton, - Card, - CardBody, - Avatar, - Flex, - Text, - Tabs, - TabList, - Tab, - Spacer, - HStack, - VStack, - ButtonGroup -} from '@chakra-ui/react' - - -import {TIMESHEET_DURATION, TIMEZONE} from 'src/constants'; - -import {Review_Stages, TABLE_COLUMNS, CommentType} from './types'; -import moment, {Moment} from 'moment-timezone'; - -import apiClient from '../Auth/apiClient'; -import AggregationTable from './AggregationTable'; -import {v4 as uuidv4} from 'uuid'; -import {UserSchema} from '../../schemas/UserSchema' - -import { SearchIcon, WarningIcon, DownloadIcon } from '@chakra-ui/icons'; -import { Select, components } from 'chakra-react-select' -import { TimeSheetSchema } from 'src/schemas/TimesheetSchema'; -import { CommentSchema, RowSchema } from 'src/schemas/RowSchema'; -import { getAllActiveCommentsOfType } from './utils'; -import { Stack } from 'react-bootstrap'; -import { Divider } from '@aws-amplify/ui-react'; - - + Alert, + AlertIcon, + AlertTitle, + AlertDescription, + Box, + IconButton, + Card, + CardBody, + Avatar, + Flex, + Text, + Tabs, + TabList, + Tab, + Spacer, + HStack, + VStack, + ButtonGroup, +} from "@chakra-ui/react"; + +import { TIMESHEET_DURATION, TIMEZONE } from "src/constants"; + +import { Review_Stages, TABLE_COLUMNS, CommentType } from "./types"; +import moment, { Moment } from "moment-timezone"; + +import apiClient from "../Auth/apiClient"; +import AggregationTable from "./AggregationTable"; +import { v4 as uuidv4 } from "uuid"; +import { UserSchema } from "../../schemas/UserSchema"; + +import { SearchIcon, WarningIcon, DownloadIcon } from "@chakra-ui/icons"; +import { Select, components } from "chakra-react-select"; +import { TimeSheetSchema } from "src/schemas/TimesheetSchema"; +import { CommentSchema, RowSchema } from "src/schemas/RowSchema"; +import { getAllActiveCommentsOfType } from "./utils"; +import { Stack } from "react-bootstrap"; +import { Divider } from "@aws-amplify/ui-react"; // Always adjust local timezone to Breaktime's timezone moment.tz.setDefault(TIMEZONE); const testingEmployees = [ - { - UserID: "abc", - FirstName: "joe", - LastName: "jane", - Type: "Employee", - Picture: "https://upload.wikimedia.org/wikipedia/commons/4/49/Koala_climbing_tree.jpg" - }, - { - UserID: "bcd", - FirstName: "david", - LastName: "lev", - Type: "Employee", - Picture: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Grosser_Panda.JPG/1200px-Grosser_Panda.JPG" - }, - { - UserID: "cde", - FirstName: "crys", - LastName: "tal", - Type: "Employee", - Picture: "https://www.google.com/capybara.png" - }, - {UserID: "def", FirstName: "ken", LastName: "ney", Type: "Employee", Picture: "https://www.google.com/koala.png"}, -] - -function ProfileCard({employee}) { - - return ( - - - - {employee?.FirstName + " " + employee?.LastName} - - - ) + { + UserID: "abc", + FirstName: "joe", + LastName: "jane", + Type: "Employee", + Picture: + "https://upload.wikimedia.org/wikipedia/commons/4/49/Koala_climbing_tree.jpg", + }, + { + UserID: "bcd", + FirstName: "david", + LastName: "lev", + Type: "Employee", + Picture: + "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Grosser_Panda.JPG/1200px-Grosser_Panda.JPG", + }, + { + UserID: "cde", + FirstName: "crys", + LastName: "tal", + Type: "Employee", + Picture: "https://www.google.com/capybara.png", + }, + { + UserID: "def", + FirstName: "ken", + LastName: "ney", + Type: "Employee", + Picture: "https://www.google.com/koala.png", + }, +]; + +function ProfileCard({ employee }) { + return ( + + + + {employee?.FirstName + " " + employee?.LastName} + + + ); } -function SearchEmployeeTimesheet({employees, setSelected}) { +function SearchEmployeeTimesheet({ employees, setSelected }) { + const handleChange = (selectedOption) => { + setSelected(selectedOption); + }; - const handleChange = (selectedOption) => { - setSelected(selectedOption); - } - - const customStyles = { - control: (base) => ({ - ...base, - flexDirection: 'row-reverse', - }), - } + const customStyles = { + control: (base) => ({ + ...base, + flexDirection: "row-reverse", + }), + }; - const DropdownIndicator = (props) => { - return ( - - - - ); - }; - - // TODO: fix styling - // at the moment defaultValue is the first user in the employees array - // which is currently an invariant that matches the useState in Page + const DropdownIndicator = (props) => { return ( -
- + `${option.FirstName + " " + option.LastName}` + } + getOptionValue={(option) => + `${option.FirstName + " " + option.LastName}` + } + /> +
+ ); } interface WeeklyCommentSectionProps { - weeklyComments: CommentSchema[]; - weeklyReports: CommentSchema[]; + weeklyComments: CommentSchema[]; + weeklyReports: CommentSchema[]; } // TODO: idk if we're keeping up just gonna remove bc doesnt look great atm function WeeklyCommentSection({ - weeklyComments, - weeklyReports - }: WeeklyCommentSectionProps) { - // row of Comments - // row of Reports - - // repetitive but readable code and should be more extensible - return ( + weeklyComments, + weeklyReports, +}: WeeklyCommentSectionProps) { + // row of Comments + // row of Reports + + // repetitive but readable code and should be more extensible + return ( + + Weekly Feedback + + - Weekly Feedback - - - - - {weeklyComments.map( - (comment) => ( - - {/* TODO: later replace w api call to get user from userID*/} - {/* also use display card once it gets merged in*/} - - {comment.Content} - - ))} - - - - - - {weeklyReports.map( - (report) => ( - - {/* TODO: later replace w api call to get user from userID*/} - - {report.Content} - - ))} - - - + + {weeklyComments.map((comment) => ( + + {/* TODO: later replace w api call to get user from userID*/} + {/* also use display card once it gets merged in*/} + + {comment.Content} + + ))} + - ) + + + + {weeklyReports.map((report) => ( + + {/* TODO: later replace w api call to get user from userID*/} + + {report.Content} + + ))} + + + + + ); } export default function Page() { - //const today = moment(); - const [selectedDate, setSelectedDate] = useState(moment().startOf('week').day(0)); - - // fetch the information of the user whos timesheet is being displayed - // if user is an employee selected and user would be the same - // if user is a supervisor/admin then selected would contain the information of the user - // whos timesheet is being looked at and user would contain the supervisor/admins information - // by default the first user is selected - const [selectedUser, setSelectedUser] = useState(); - const [user, setUser] = useState(); - - // associates is only used by supervisor/admin for the list of all associates they have access to - const [associates, setAssociates] = useState([]); - - // A list of the timesheet objects - // TODO: add types - const [userTimesheets, setUserTimesheets] = useState([]); - const [currentTimesheets, setCurrentTimesheets] = useState([]); - const [selectedTimesheet, setTimesheet] = useState(undefined); - - const [weeklyComments, setWeeklyComments] = useState([]); - const [weeklyReports, setWeeklyReports] = useState([]); - - // this hook should always run first - useEffect(() => { - apiClient.getUser().then(userInfo => { - setUser(userInfo); - if (userInfo.Type === "Supervisor" || userInfo.Type === "Admin") { - apiClient.getAllUsers().then(users => { - setAssociates(users); - setSelectedUser(users[0]); - }) - } - setSelectedUser(userInfo) - }) - // if employee setSelectedUSer to be userinfo - // if supervisor/admin get all users - // set selected user - }, []) - - // Pulls user timesheets, marking first returned as the active one - useEffect(() => { - apiClient.getUserTimesheets(selectedUser?.UserID).then(timesheets => { - setUserTimesheets(timesheets); - //By Default just render / select the first timesheet for now - setCurrentTimesheetsToDisplay(timesheets, selectedDate); - }); - }, [selectedUser]) - - const processTimesheetChange = (updated_sheet) => { - // Updating the rows of the selected timesheets from our list of timesheets - const modifiedTimesheets = userTimesheets.map((entry) => { - if (entry.TimesheetID === selectedTimesheet.TimesheetID) { - return { - ...entry, - TableData: updated_sheet.TableData - } - } - return entry + //const today = moment(); + const [selectedDate, setSelectedDate] = useState( + moment().startOf("week").day(0) + ); + + // fetch the information of the user whos timesheet is being displayed + // if user is an employee selected and user would be the same + // if user is a supervisor/admin then selected would contain the information of the user + // whos timesheet is being looked at and user would contain the supervisor/admins information + // by default the first user is selected + const [selectedUser, setSelectedUser] = useState(); + const [user, setUser] = useState(); + + // associates is only used by supervisor/admin for the list of all associates they have access to + const [associates, setAssociates] = useState([]); + + // A list of the timesheet objects + // TODO: add types + const [userTimesheets, setUserTimesheets] = useState([]); + const [currentTimesheets, setCurrentTimesheets] = useState([]); + const [selectedTimesheet, setTimesheet] = useState(undefined); + + const [weeklyComments, setWeeklyComments] = useState([]); + const [weeklyReports, setWeeklyReports] = useState([]); + + // this hook should always run first + useEffect(() => { + // Get the current logged in user + getCurrentUser().then((currentUser) => { + console.log("Current user: ", currentUser); + setUser(currentUser); + }); + + apiClient.getUser().then((userInfo) => { + if (userInfo.Type === "Supervisor" || userInfo.Type === "Admin") { + apiClient.getAllUsers().then((users) => { + setAssociates(users); + setSelectedUser(users[0]); }); - setUserTimesheets(modifiedTimesheets); - - //Also need to update our list of currently selected - TODO come up with a way to not need these duplicated lists - setCurrentTimesheets(currentTimesheets.map( - (entry) => { - if (entry.TimesheetID === selectedTimesheet.TimesheetID) { - return { - ...entry, - TableData: updated_sheet.TableData - } - } - return entry - } - )); + } + setSelectedUser(userInfo); + }); + // if employee setSelectedUSer to be userinfo + // if supervisor/admin get all users + // set selected user + }, []); + + const getUpdatedTimesheet = (userId) => { + apiClient.getUserTimesheets(userId).then((timesheets) => { + setUserTimesheets(timesheets); + //By Default just render / select the first timesheet for now + setCurrentTimesheetsToDisplay(timesheets, selectedDate); + }); + }; + + // Pulls user timesheets, marking first returned as the active one + useEffect(() => { + getUpdatedTimesheet(selectedUser?.UserID); + }, [selectedUser]); + + // Callback function that triggers GET request to the API to grab the most recent version of timesheets for the current selected user, + // and re-sets the current timesheet state variable + const forceRefreshTimesheet = () => { + if (selectedUser !== undefined) { + getUpdatedTimesheet(selectedUser.UserID); + } else { + console.log("ERROR: Couldn't force refresh timesheet"); } - - const updateDateRange = (date: Moment) => { - setSelectedDate(date); - //TODO - Refactor this to use the constant in merge with contants branch - setCurrentTimesheetsToDisplay(userTimesheets, date); - } - - const changeTimesheet = (sheet) => { - setTimesheet(sheet) - setWeeklyComments(getAllActiveCommentsOfType(CommentType.Comment, sheet.WeekNotes)) - setWeeklyReports(getAllActiveCommentsOfType(CommentType.Report, sheet.WeekNotes)) - } - - - const setCurrentTimesheetsToDisplay = (timesheets, currentStartDate: Moment) => { - const newCurrentTimesheets = timesheets.filter(sheet => moment.unix(sheet.StartDate).isSame(currentStartDate, 'day')); - - setCurrentTimesheets(newCurrentTimesheets); - if (newCurrentTimesheets.length > 0) { - changeTimesheet(newCurrentTimesheets[0]) + }; + + const processTimesheetChange = (updated_sheet) => { + // Updating the rows of the selected timesheets from our list of timesheets + const modifiedTimesheets = userTimesheets.map((entry) => { + if (entry.TimesheetID === selectedTimesheet.TimesheetID) { + return { + ...entry, + TableData: updated_sheet.TableData, + }; + } + return entry; + }); + setUserTimesheets(modifiedTimesheets); + + //Also need to update our list of currently selected - TODO come up with a way to not need these duplicated lists + setCurrentTimesheets( + currentTimesheets.map((entry) => { + if (entry.TimesheetID === selectedTimesheet.TimesheetID) { + return { + ...entry, + TableData: updated_sheet.TableData, + }; } + return entry; + }) + ); + }; + + const updateDateRange = (date: Moment) => { + setSelectedDate(date); + //TODO - Refactor this to use the constant in merge with contants branch + setCurrentTimesheetsToDisplay(userTimesheets, date); + }; + + const changeTimesheet = (sheet) => { + setTimesheet(sheet); + setWeeklyComments( + getAllActiveCommentsOfType(CommentType.Comment, sheet.WeekNotes) + ); + setWeeklyReports( + getAllActiveCommentsOfType(CommentType.Report, sheet.WeekNotes) + ); + }; + + const setCurrentTimesheetsToDisplay = ( + timesheets, + currentStartDate: Moment + ) => { + const newCurrentTimesheets = timesheets.filter((sheet) => + moment.unix(sheet.StartDate).isSame(currentStartDate, "day") + ); + + setCurrentTimesheets(newCurrentTimesheets); + if (newCurrentTimesheets.length > 0) { + changeTimesheet(newCurrentTimesheets[0]); } - - const renderWarning = () => { - const currentDate = moment(); - - const dateToCheck = moment(selectedDate); - dateToCheck.add(TIMESHEET_DURATION, 'days'); - if (currentDate.isAfter(dateToCheck, 'days')) { - return - - Your timesheet is late! - Please submit this as soon as possible - - } else { - const dueDuration = dateToCheck.diff(currentDate, 'days'); - return - - Your timesheet is due in {dueDuration} days! - Remember to press the submit button! - - } + }; + + const renderWarning = () => { + const currentDate = moment(); + + const dateToCheck = moment(selectedDate); + dateToCheck.add(TIMESHEET_DURATION, "days"); + if (currentDate.isAfter(dateToCheck, "days")) { + return ( + + + Your timesheet is late! + + Please submit this as soon as possible + + + ); + } else { + const dueDuration = dateToCheck.diff(currentDate, "days"); + return ( + + + Your timesheet is due in {dueDuration} days! + + Remember to press the submit button! + + + ); } + }; // use this to control whether the timesheet is disabled or not - const disabled = false - - - return ( - <> - - - {(user?.Type === "Supervisor" || user?.Type === "Admin") ? - <> - - }/> - }/> - : <>} - - - - - {useMemo(() => renderWarning(), [selectedDate])} + const disabled = false; + + return ( + + <> + + + {user?.Type === "Supervisor" || user?.Type === "Admin" ? ( + <> + + } /> + } /> + + ) : ( + <> + )} + + {selectedTimesheet && ( + + )} + + {useMemo(() => renderWarning(), [selectedDate])}
- - - {currentTimesheets.map( - (sheet) => ( - changeTimesheet(sheet)}>{sheet.CompanyID} - ) - )} - - + + + {currentTimesheets.map((sheet) => ( + changeTimesheet(sheet)}> + {sheet.CompanyID} + + ))} + +
- {selectedTimesheet?.CompanyID === "Total" ? - () - : ( - - )} -
- -
- - ) -} \ No newline at end of file + {selectedTimesheet?.CompanyID === "Total" ? ( + + ) : ( + + + + )} + + + +
+ ); +} diff --git a/apps/frontend/src/components/TimeCardPage/types.tsx b/apps/frontend/src/components/TimeCardPage/types.tsx index 653ec37..9c3f22a 100644 --- a/apps/frontend/src/components/TimeCardPage/types.tsx +++ b/apps/frontend/src/components/TimeCardPage/types.tsx @@ -1,44 +1,57 @@ export enum CellType { - Regular = "Time Worked", - PTO = "PTO" -}; + Regular = "Time Worked", + PTO = "PTO", +} export enum CellStatus { - Active = "Active", - Deleted = "Deleted" + Active = "Active", + Deleted = "Deleted", } export enum CommentType { - Comment = "Comment", - Report = "Report", -}; + Comment = "Comment", + Report = "Report", +} export enum ReportOptions { - Late = "Late Arrival", - LeftEarly = "Early Departure", - Absent = "No Show" + Late = "Late Arrival", + LeftEarly = "Early Departure", + Absent = "No Show", } export enum Color { - Red = "red", - Blue = "blue", - Green = "green", - Gray = "gray" + Red = "red", + Blue = "blue", + Green = "green", + Gray = "gray", } export const enum Review_Stages { - UNSUBMITTED = "Not-Submitted", - EMPLOYEE_SUBMITTED = "Employee Submitted", - ADMIN_REVIEW = "Review (Breaktime)", - APPROVED = "Approved" -}; + UNSUBMITTED = "Not-Submitted", + EMPLOYEE_SUBMITTED = "Employee Submitted", + ADMIN_REVIEW = "Review (Breaktime)", + APPROVED = "Approved", +} -export const TABLE_COLUMNS = ['Type', 'Date', 'Clock-in', 'Clock-Out', 'Hours', 'Comment']; +export const TABLE_COLUMNS = [ + "Type", + "Date", + "Clock-in", + "Clock-Out", + "Hours", + "Comment", +]; export enum CardState { - Rejected = "Rejected", - InReviewEmployer = "In Review - Employer", - InReviewBreaktime = "In Review - Breaktime", - Completed = "Completed", - Unsubmitted = "Unsubmitted" -} \ No newline at end of file + Rejected = "Rejected", + InReviewSupervisor = "In Review - Supervisor", + InReviewAdmin = "In Review - Admin", + AdminFinalized = "Finalized by Admin", + Unsubmitted = "Unsubmitted", +} + +export enum UserTypes { + Associate = "Associate", + Supervisor = "Supervisor", + Admin = "Admin", +} diff --git a/apps/frontend/src/schemas/StatusSchema.tsx b/apps/frontend/src/schemas/StatusSchema.tsx new file mode 100644 index 0000000..f5b5b62 --- /dev/null +++ b/apps/frontend/src/schemas/StatusSchema.tsx @@ -0,0 +1,21 @@ +import { z } from "zod"; + +// The status is either undefined, for not being at that stage yet, or +// contains the date and author of approving this submission +export const StatusEntryType = z.union([ + z.object({ + Date: z.number(), + AuthorID: z.string(), + }), + z.undefined(), +]); + +// Status type contains the four stages of the pipeline we have defined +export const StatusType = z.object({ + HoursSubmitted: StatusEntryType, + HoursReviewed: StatusEntryType, + Finalized: StatusEntryType, +}); + +export type StatusEntryType = z.infer; +export type StatusType = z.infer; diff --git a/apps/frontend/src/schemas/TimesheetSchema.tsx b/apps/frontend/src/schemas/TimesheetSchema.tsx index bbd835e..3ce5e00 100644 --- a/apps/frontend/src/schemas/TimesheetSchema.tsx +++ b/apps/frontend/src/schemas/TimesheetSchema.tsx @@ -1,32 +1,22 @@ import { z } from "zod"; -import { RowSchema, ScheduledRowSchema, CommentSchema } from './RowSchema'; - -// The status is either undefined, for not being at that stage yet, or -// contains the date and author of approving this submission -export const StatusEntryType = z.union( - [z.object({ - Date: z.number(), - AuthorID: z.string() - }), - z.undefined()]); - -// Status type contains the four stages of the pipeline we have defined -export const StatusType = z.object({ - HoursSubmitted: StatusEntryType, - HoursReviewed: StatusEntryType, - ScheduleSubmitted: StatusEntryType, - Finalized: StatusEntryType -}); +import { RowSchema, ScheduledRowSchema, CommentSchema } from "./RowSchema"; +import { StatusType } from "./StatusSchema"; export const TimeSheetSchema = z.object({ - TimesheetID: z.number(), - UserID: z.string(), + TimesheetID: z.number(), + UserID: z.string(), StartDate: z.number(), - Status: StatusType, - CompanyID: z.string(), - TableData: z.array(RowSchema), + Status: StatusType, + CompanyID: z.string(), + TableData: z.array(RowSchema), ScheduleTableData: z.union([z.undefined(), z.array(ScheduledRowSchema)]), - WeekNotes: z.union([z.undefined(), z.array(CommentSchema)]), -}); + WeekNotes: z.union([z.undefined(), z.array(CommentSchema)]), +}); export type TimeSheetSchema = z.infer; + +export enum TimesheetStatus { + HOURS_SUBMITTED = "HoursSubmitted", + HOURS_REVIEWED = "HoursReviewed", + FINALIZED = "Finalized", +} diff --git a/apps/frontend/src/schemas/UserSchema.tsx b/apps/frontend/src/schemas/UserSchema.tsx index a51af17..e73e4c0 100644 --- a/apps/frontend/src/schemas/UserSchema.tsx +++ b/apps/frontend/src/schemas/UserSchema.tsx @@ -1,10 +1,11 @@ import { z } from "zod"; +import { UserTypes } from "src/components/TimeCardPage/types"; export const UserSchema = z.object({ UserID: z.string(), FirstName: z.string(), LastName: z.string(), - Type: z.enum(["Associate", "Supervisor", "Admin"]), + Type: z.enum([UserTypes.Associate, UserTypes.Supervisor, UserTypes.Admin]), Picture: z.string().optional(), }); diff --git a/apps/frontend/src/schemas/backend/Timesheet.ts b/apps/frontend/src/schemas/backend/Timesheet.ts index 949513e..3eae7e2 100644 --- a/apps/frontend/src/schemas/backend/Timesheet.ts +++ b/apps/frontend/src/schemas/backend/Timesheet.ts @@ -43,7 +43,11 @@ export const TimeEntrySchema = z.object({ AuthorUUID: z.string(), }) - +/* + Supported type of cells for each row in a timesheet + @REGULAR - a regular cell + @PTO - Cell signifying paid time off (PTO) +*/ export enum CellType { REGULAR = "Time Worked", PTO = "PTO" @@ -72,14 +76,18 @@ export const StatusEntryType = z.union( }), z.undefined()]); -// Status type contains the four stages of the pipeline we have defined -export const TimesheetStatus = z.object({ +// Status type contains the three stages of the pipeline we have defined +export const TimesheetStatusSchema = z.object({ HoursSubmitted: StatusEntryType, HoursReviewed: StatusEntryType, - ScheduleSubmitted: StatusEntryType, Finalized: StatusEntryType }); +export enum TimesheetStatusType { + HOURS_SUBMITTED="HoursSubmitted", + HOURS_REVIEWED="HoursReviewed", + FINALIZED="Finalized" +} /** @@ -89,14 +97,14 @@ export const TimeSheetSchema = z.object({ TimesheetID: z.number(), UserID: z.string(), StartDate: z.number(), - Status: TimesheetStatus, + Status: TimesheetStatusSchema, CompanyID: z.string(), HoursData: z.array(TimesheetEntrySchema).default([]), ScheduleData: z.array(ScheduleEntrySchema).default([]), WeekNotes: z.array(NoteSchema).default([]), }) -export type TimesheetStatus = z.infer +export type TimesheetStatus = z.infer export type TimeEntrySchema = z.infer export type ScheduleEntrySchema = z.infer export type NoteSchema = z.infer diff --git a/apps/frontend/src/schemas/backend/UpdateTimesheet.ts b/apps/frontend/src/schemas/backend/UpdateTimesheet.ts index 851382d..5cc7c0c 100644 --- a/apps/frontend/src/schemas/backend/UpdateTimesheet.ts +++ b/apps/frontend/src/schemas/backend/UpdateTimesheet.ts @@ -12,7 +12,16 @@ import * as dbtypes from './Timesheet' */ -// Currently supported timesheet operations +/* + The supported timesheet operations currently supported. + Most operations relate to items that are inside the timesheet, whether it is the rows of the timesheet, the comments someone left + on it for example. + INSERT - Inserting an item into the timesheet + UPDATE - Updating a specific item in the timesheet + DELETE - Deleting a speciic item in the timesheet + STATUS_CHANGE - When the timesheet has been submitted / should be advanced to the next stage + CREATE-TIMESHEET - Operation for creating a timesheet, if it would be useful to have in the future. +*/ export const enum TimesheetOperations { INSERT = "INSERT", UPDATE = "UPDATE", @@ -21,8 +30,12 @@ export const enum TimesheetOperations { CREATE_TIMESHEET = "CREATE_TIMESHEET" } - - +/* + The available types of items that are currently supported in the timesheet that list operations can be performed on. + TABLEDATA - the rows of the timesheet- basically their worked schedule + SCHEDULEDATA - the expected schedule they should have worked + WEEKNOTES - the comments left by an employer for that week +*/ export const enum TimesheetListItems { TABLEDATA = "TABLEDATA", SCHEDULEDATA = "SCHEDULEDATA", @@ -31,23 +44,34 @@ export const enum TimesheetListItems { const availableListTypes = z.enum([TimesheetListItems.TABLEDATA, TimesheetListItems.SCHEDULEDATA, TimesheetListItems.WEEKNOTES]) +/* + The schema for a delete request + @Type: The type of the item this delete request is processing - see available types in TimesheetListItems + @Id: The id of the item we are deleting - to know what to remove +*/ export const DeleteRequest = z.object({ Type: availableListTypes, Id: z.string() }) export type DeleteRequest = z.infer +/* + The schema for an insert request for an item + @Type: The type of the item that we are inserting, to know what we should be adding this item to + @Item: The item we are actually inserting, should be the actual item itself. +*/ export const InsertRequest = z.object({ Type: availableListTypes, Item: z.union([RowSchema, CommentSchema, ScheduledRowSchema, dbtypes.TimesheetEntrySchema]), }) export type InsertRequest = z.infer + /* Schema for updating an item from the three possible list of items in the timesheet - Type: The field of the timesheet we are updating from the three supported - Id: the id of the entry we are updating - correlates to that row / entry in the list of items - Attribute: The specific attribute of the object we are updating - Data: The payload we are updating this attribute to be - can be a wide range of things currently + @Type: The field of the timesheet we are updating from the three supported + @Id: the id of the entry we are updating - correlates to that row / entry in the list of items + @Attribute: The specific attribute of the object we are updating + @Data: The payload we are updating this attribute to be - can be a wide range of things currently */ export const UpdateRequest = z.object({ Type: availableListTypes, @@ -57,8 +81,28 @@ export const UpdateRequest = z.object({ }) export type UpdateRequest = z.infer +/* + Schema for changing the status of a timesheet + @TimesheetId: The id of the timesheet we are updating + @AssociateId: The id of the associate whose timesheet is being submitted + @authorId: + @dateSubmitted: The date the timesheet was submitted + @statusType: +*/ +export const StatusChangeRequest = z.object({ + TimesheetId: z.number(), + AssociateId: z.string(), + authorId: z.string(), + dateSubmitted: z.number(), + statusType: z.enum([dbtypes.TimesheetStatusType.HOURS_SUBMITTED, dbtypes.TimesheetStatusType.HOURS_REVIEWED, dbtypes.TimesheetStatusType.FINALIZED]), +}) +export type StatusChangeRequest = z.infer -// The main request body that is used to determine what we should be updating in a request +/* The main request body that is used to determine what we should be updating in a request + @TimesheetID: The id of the timesheet we are updating + @Operation: The type of operation we are performing on this timesheet + @Payload: The contents to be used in the operation for updating this. +*/ export const TimesheetUpdateRequest = z.object({ TimesheetID: z.number(), Operation: z.enum([ From 2b87786269cb8388cb089ab858e1447286a817c1 Mon Sep 17 00:00:00 2001 From: Izzy Conner <32255130+izzyconner@users.noreply.github.com> Date: Sun, 11 Feb 2024 11:51:22 -0500 Subject: [PATCH 06/14] Create pull_request_template.md --- .github/pull_request_template.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..e79c36a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +ℹ️ Issue Closes clickup ticket link + +📝 Description Briefly list the changes made to the code: + +Added support for this. And removed redunant use of that. Also this was included for reasons. + +✔️ Testing What steps did you take to verify your changes work? These should be clear enough for someone to be able to clone the branch and follow the steps themselves. + +Provide screenshots of any new components, styling changes, or pages. + +🏕️ (Optional) Future Work / Notes Did you notice anything ugly during the course of this ticket? Any bugs, design challenges, or unexpected behavior? Write it down so we can clean it up in a future ticket! From b40f548e42e1e9932bb4341274a6e0cb6fdcc519 Mon Sep 17 00:00:00 2001 From: izzyconner <32255130+izzyconner@users.noreply.github.com> Date: Sun, 11 Feb 2024 12:08:01 -0500 Subject: [PATCH 07/14] Show submission status regardless of if current user has submitted --- apps/frontend/src/components/TimeCardPage/SubmitCard.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx b/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx index 5be3796..c59e406 100644 --- a/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx +++ b/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx @@ -78,7 +78,7 @@ export default function SubmitCard(props: submitCardProps) { } else { setState(CardState.Unsubmitted); } - }, [currUser, props.timesheetStatus]); + }, [currUser, props.timesheetStatus, props.timesheetId]); const submitAction = async () => { console.log("Current user id:", currUser.UserID); @@ -160,7 +160,6 @@ export default function SubmitCard(props: submitCardProps) { {submitted ? "Resubmit" : "Submit!"} - {submitted && ( {/* TODO: The AuthorIDs below should all be replaced with calls to the API and then have a User profile card there instead (or at least the name, rather than ID lol) */}
@@ -200,7 +199,6 @@ export default function SubmitCard(props: submitCardProps) { )}
- )} ); From dbe7a7888fb49129dd2ebdf478a02a7d9b84755d Mon Sep 17 00:00:00 2001 From: izzyconner <32255130+izzyconner@users.noreply.github.com> Date: Sun, 11 Feb 2024 16:20:37 -0500 Subject: [PATCH 08/14] cleanup changes and consolidating CardState and Status enums --- .../src/components/Auth/UserUtils.tsx | 1 - .../components/TimeCardPage/SubmitCard.tsx | 104 +++++++++--------- .../src/components/TimeCardPage/TimeSheet.tsx | 2 +- .../src/components/TimeCardPage/types.tsx | 15 --- apps/frontend/src/schemas/RowSchema.tsx | 88 ++++++++------- apps/frontend/src/schemas/TimesheetSchema.tsx | 1 + 6 files changed, 102 insertions(+), 109 deletions(-) diff --git a/apps/frontend/src/components/Auth/UserUtils.tsx b/apps/frontend/src/components/Auth/UserUtils.tsx index b15d694..aa03e6b 100644 --- a/apps/frontend/src/components/Auth/UserUtils.tsx +++ b/apps/frontend/src/components/Auth/UserUtils.tsx @@ -41,7 +41,6 @@ const convertCognitoUser = function (user: CognitoUser): UserSchema { }; }; -// TODO: Should this be in a separate file because export const getCurrentUser = async function (): Promise { const cognitoCurrentUser = await Auth.currentUserInfo(); console.log(cognitoCurrentUser); diff --git a/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx b/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx index c59e406..2cdfd4e 100644 --- a/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx +++ b/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx @@ -1,5 +1,3 @@ -import { CardState } from "./types"; - import React, { useState, useEffect, useContext } from "react"; import { Box, Card, CardBody, CardFooter, Button } from "@chakra-ui/react"; import { DEFAULT_COLORS } from "src/constants"; @@ -24,7 +22,7 @@ export default function SubmitCard(props: submitCardProps) { const currUser = useContext(UserContext); /** Whether or not the logged-in user has submitted this timesheet yet.*/ - const [submitted, setSubmitted] = useState(false); + const [timesheetSubmitted, setTimesheetSubmitted] = useState(false); /** The date and time (as a moment) that the logged-in user submitted/reviewed/finalized this timesheet.*/ const [submitDate, setSubmitDate] = useState(null); @@ -35,7 +33,7 @@ export default function SubmitCard(props: submitCardProps) { * the supervisor had submitted their timesheet review, the card state would be CardState.InReviewAdmin for * any associate, supervisor, or admin that was viewing the timesheet. */ - const [state, setState] = useState(CardState.Unsubmitted); + const [cardState, setCardState] = useState(TimesheetStatus.UNSUBMITTED); // Run whenever there's an update to the current logged in user (i.e. re-render the correct submission status for the user type) useEffect(() => { @@ -61,7 +59,7 @@ export default function SubmitCard(props: submitCardProps) { } const isSubmitted = statusEntry !== undefined; - setSubmitted(isSubmitted); + setTimesheetSubmitted(isSubmitted); // Set the submitted date to when the corresponding status entry was logged for the user type if (isSubmitted && statusEntry.Date !== undefined) { @@ -70,13 +68,13 @@ export default function SubmitCard(props: submitCardProps) { // Determine the latest status update to set the card state if (props.timesheetStatus.Finalized !== undefined) { - setState(CardState.AdminFinalized); + setCardState(TimesheetStatus.FINALIZED); } else if (props.timesheetStatus.HoursReviewed !== undefined) { - setState(CardState.InReviewAdmin); + setCardState(TimesheetStatus.HOURS_REVIEWED); } else if (props.timesheetStatus.HoursSubmitted !== undefined) { - setState(CardState.InReviewSupervisor); + setCardState(TimesheetStatus.HOURS_SUBMITTED); } else { - setState(CardState.Unsubmitted); + setCardState(TimesheetStatus.UNSUBMITTED); } }, [currUser, props.timesheetStatus, props.timesheetId]); @@ -102,6 +100,8 @@ export default function SubmitCard(props: submitCardProps) { // Update the current timesheet to be submitted by the logged-in user. // The type of status can be determined on the backend by the user type try { + // TODO: the parsing of the TimesheetUpdateRequest should really be done in the apiClient - ideally, we don't deal w/ backend schemas at all anywhere other than + // the ApiClient file const response = await ApiClient.updateTimesheet( updateSchemas.TimesheetUpdateRequest.parse({ TimesheetID: props.timesheetId, @@ -144,10 +144,10 @@ export default function SubmitCard(props: submitCardProps) { > - - {/* TODO: The AuthorIDs below should all be replaced with calls to the API and then have a User profile card there instead (or at least the name, rather than ID lol) */} -
- {props.timesheetStatus.HoursSubmitted && - props.timesheetStatus.HoursSubmitted.Date ? ( -

- Associate: {props.timesheetStatus.HoursSubmitted.AuthorID},{" "} -
- Submitted on:{" "} - {customDateFormat(props.timesheetStatus.HoursSubmitted.Date)} -

- ) : ( -

Associate: Unsubmitted

- )} - - {props.timesheetStatus.HoursReviewed && - props.timesheetStatus.HoursReviewed.Date ? ( -

- Supervsior: {props.timesheetStatus.HoursReviewed.AuthorID},{" "} -
- Submitted on:{" "} - {customDateFormat(props.timesheetStatus.HoursReviewed.Date)} -

- ) : ( -

Supervisor: Unsubmitted

- )} - - {props.timesheetStatus.Finalized && - props.timesheetStatus.Finalized.Date ? ( -

- Admin: {props.timesheetStatus.Finalized.AuthorID},
- Submitted on:{" "} - {customDateFormat(props.timesheetStatus.Finalized.Date)} -

- ) : ( -

Admin: Unsubmitted

- )} -
-
+ + {/* TODO: The AuthorIDs below should all be replaced with calls to the API and then have a User profile card there instead (or at least the name, rather than ID lol) */} +
+ {props.timesheetStatus.HoursSubmitted && + props.timesheetStatus.HoursSubmitted.Date ? ( +

+ Associate: {props.timesheetStatus.HoursSubmitted.AuthorID},{" "} +
+ Submitted on:{" "} + {customDateFormat(props.timesheetStatus.HoursSubmitted.Date)} +

+ ) : ( +

Associate: Unsubmitted

+ )} + + {props.timesheetStatus.HoursReviewed && + props.timesheetStatus.HoursReviewed.Date ? ( +

+ Supervsior: {props.timesheetStatus.HoursReviewed.AuthorID},{" "} +
+ Submitted on:{" "} + {customDateFormat(props.timesheetStatus.HoursReviewed.Date)} +

+ ) : ( +

Supervisor: Unsubmitted

+ )} + + {props.timesheetStatus.Finalized && + props.timesheetStatus.Finalized.Date ? ( +

+ Admin: {props.timesheetStatus.Finalized.AuthorID},
+ Submitted on:{" "} + {customDateFormat(props.timesheetStatus.Finalized.Date)} +

+ ) : ( +

Admin: Unsubmitted

+ )} +
+
); diff --git a/apps/frontend/src/components/TimeCardPage/TimeSheet.tsx b/apps/frontend/src/components/TimeCardPage/TimeSheet.tsx index 807f0ae..dc3c5f5 100644 --- a/apps/frontend/src/components/TimeCardPage/TimeSheet.tsx +++ b/apps/frontend/src/components/TimeCardPage/TimeSheet.tsx @@ -29,7 +29,7 @@ import { import { TIMESHEET_DURATION, TIMEZONE } from "src/constants"; -import { Review_Stages, TABLE_COLUMNS, CommentType } from "./types"; +import { TABLE_COLUMNS, CommentType } from "./types"; import moment, { Moment } from "moment-timezone"; import apiClient from "../Auth/apiClient"; diff --git a/apps/frontend/src/components/TimeCardPage/types.tsx b/apps/frontend/src/components/TimeCardPage/types.tsx index 9c3f22a..6301e9e 100644 --- a/apps/frontend/src/components/TimeCardPage/types.tsx +++ b/apps/frontend/src/components/TimeCardPage/types.tsx @@ -26,13 +26,6 @@ export enum Color { Gray = "gray", } -export const enum Review_Stages { - UNSUBMITTED = "Not-Submitted", - EMPLOYEE_SUBMITTED = "Employee Submitted", - ADMIN_REVIEW = "Review (Breaktime)", - APPROVED = "Approved", -} - export const TABLE_COLUMNS = [ "Type", "Date", @@ -42,14 +35,6 @@ export const TABLE_COLUMNS = [ "Comment", ]; -export enum CardState { - Rejected = "Rejected", - InReviewSupervisor = "In Review - Supervisor", - InReviewAdmin = "In Review - Admin", - AdminFinalized = "Finalized by Admin", - Unsubmitted = "Unsubmitted", -} - export enum UserTypes { Associate = "Associate", Supervisor = "Supervisor", diff --git a/apps/frontend/src/schemas/RowSchema.tsx b/apps/frontend/src/schemas/RowSchema.tsx index 1e98d4f..f47244c 100644 --- a/apps/frontend/src/schemas/RowSchema.tsx +++ b/apps/frontend/src/schemas/RowSchema.tsx @@ -1,55 +1,63 @@ import { z } from "zod"; -import {CellType, CommentType, Review_Stages, CellStatus, ReportOptions} from '../components/TimeCardPage/types'; +import { + CellType, + CommentType, + CellStatus, + ReportOptions, +} from "../components/TimeCardPage/types"; const optionalNumber = z.union([z.undefined(), z.number()]); const optionalString = z.union([z.undefined(), z.string()]); - - -export const TimeRowEntry = z.union([z.undefined(), z.object({ - Start: optionalNumber, End: optionalNumber, AuthorID: optionalString -})]); -export type TimeRowEntry = z.infer +export const TimeRowEntry = z.union([ + z.undefined(), + z.object({ + Start: optionalNumber, + End: optionalNumber, + AuthorID: optionalString, + }), +]); +export type TimeRowEntry = z.infer; export const CommentSchema = z.object({ - UUID: z.string(), - AuthorID:z.string(), - Type: z.nativeEnum(CommentType), // remove this - Timestamp: z.number(), - Content: z.string(), - State: z.nativeEnum(CellStatus), -}); + UUID: z.string(), + AuthorID: z.string(), + Type: z.nativeEnum(CommentType), // remove this + Timestamp: z.number(), + Content: z.string(), + State: z.nativeEnum(CellStatus), +}); -export type CommentSchema = z.infer +export type CommentSchema = z.infer; export const ReportSchema = z.object({ - AuthorID:z.string(), - Timestamp: z.number(), - Type: z.nativeEnum(CommentType), - CorrectTime: z.number(), - Content: z.nativeEnum(ReportOptions), - Notified: z.string(), - Explanation: z.string(), - State: z.nativeEnum(CellStatus), -}); - -export type ReportSchema = z.infer + AuthorID: z.string(), + Timestamp: z.number(), + Type: z.nativeEnum(CommentType), + CorrectTime: z.number(), + Content: z.nativeEnum(ReportOptions), + Notified: z.string(), + Explanation: z.string(), + State: z.nativeEnum(CellStatus), +}); + +export type ReportSchema = z.infer; export const RowSchema = z.object({ - Type: z.nativeEnum(CellType), - UUID: z.string(), - Date: z.number(), - Associate: TimeRowEntry, - Supervisor: TimeRowEntry, - Admin: TimeRowEntry, - Comment: z.union([z.undefined(), z.array(CommentSchema || ReportSchema)]) -}); -export type RowSchema = z.infer + Type: z.nativeEnum(CellType), + UUID: z.string(), + Date: z.number(), + Associate: TimeRowEntry, + Supervisor: TimeRowEntry, + Admin: TimeRowEntry, + Comment: z.union([z.undefined(), z.array(CommentSchema || ReportSchema)]), +}); +export type RowSchema = z.infer; export const ScheduledRowSchema = z.object({ - UUID: z.string(), - Date: z.number(), - Entry: TimeRowEntry -}); + UUID: z.string(), + Date: z.number(), + Entry: TimeRowEntry, +}); -export type ScheduledRowSchema = z.infer +export type ScheduledRowSchema = z.infer; diff --git a/apps/frontend/src/schemas/TimesheetSchema.tsx b/apps/frontend/src/schemas/TimesheetSchema.tsx index 3ce5e00..e278cd0 100644 --- a/apps/frontend/src/schemas/TimesheetSchema.tsx +++ b/apps/frontend/src/schemas/TimesheetSchema.tsx @@ -16,6 +16,7 @@ export const TimeSheetSchema = z.object({ export type TimeSheetSchema = z.infer; export enum TimesheetStatus { + UNSUBMITTED = "Unsubmitted", HOURS_SUBMITTED = "HoursSubmitted", HOURS_REVIEWED = "HoursReviewed", FINALIZED = "Finalized", From dc575ebc62d0edd449e38414c8ca0489c6f34afe Mon Sep 17 00:00:00 2001 From: izzyconner <32255130+izzyconner@users.noreply.github.com> Date: Sun, 11 Feb 2024 16:26:48 -0500 Subject: [PATCH 09/14] switching to moment for timestamp formatting in submitcard --- .../src/components/TimeCardPage/SubmitCard.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx b/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx index 2cdfd4e..bfd7109 100644 --- a/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx +++ b/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx @@ -204,11 +204,7 @@ export default function SubmitCard(props: submitCardProps) { ); } -function customDateFormat(date) { - const dateX = new Date(date * 1000); - return dateX.toLocaleDateString("en-US", { - year: "2-digit", - month: "2-digit", - day: "2-digit", - }); +/** Standardized moment.format() wrapper for epoch to string timestamps */ +function customDateFormat(date: number): string { + return moment.unix(date).format("MM/DD/YYYY HH:mm"); } From 3d1c674dbaf39caa57098375605a2bc97519dd63 Mon Sep 17 00:00:00 2001 From: izzyconner <32255130+izzyconner@users.noreply.github.com> Date: Sun, 11 Feb 2024 16:39:57 -0500 Subject: [PATCH 10/14] adding createToast wrapper for submitCard toast notifs --- .../components/TimeCardPage/SubmitCard.tsx | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx b/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx index bfd7109..0f675fe 100644 --- a/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx +++ b/apps/frontend/src/components/TimeCardPage/SubmitCard.tsx @@ -9,6 +9,7 @@ import moment from "moment"; import { useToast } from "@chakra-ui/react"; import { UserContext } from "./UserContext"; import { TimesheetStatus } from "src/schemas/TimesheetSchema"; +import { createToast } from "./utils"; interface submitCardProps { timesheetId: number; @@ -24,9 +25,6 @@ export default function SubmitCard(props: submitCardProps) { /** Whether or not the logged-in user has submitted this timesheet yet.*/ const [timesheetSubmitted, setTimesheetSubmitted] = useState(false); - /** The date and time (as a moment) that the logged-in user submitted/reviewed/finalized this timesheet.*/ - const [submitDate, setSubmitDate] = useState(null); - /** * The card state which corresponds to the latest status update from the timesheet. Corresponds to card color. * Note that this is *not* dependent on the logged in user. I.e. if the latest status update was that @@ -61,11 +59,6 @@ export default function SubmitCard(props: submitCardProps) { const isSubmitted = statusEntry !== undefined; setTimesheetSubmitted(isSubmitted); - // Set the submitted date to when the corresponding status entry was logged for the user type - if (isSubmitted && statusEntry.Date !== undefined) { - setSubmitDate(moment.unix(statusEntry.Date)); - } - // Determine the latest status update to set the card state if (props.timesheetStatus.Finalized !== undefined) { setCardState(TimesheetStatus.FINALIZED); @@ -119,20 +112,24 @@ export default function SubmitCard(props: submitCardProps) { // TODO: Confirm successful 2xx code responSse from API props.refreshTimesheetCallback(); - toast({ - title: "Successful submission!", - status: "success", - duration: 3000, - isClosable: true, - }); + toast( + createToast({ + title: "Successful submission!", + status: "success", + duration: 3000, + isClosable: true, + }) + ); } catch (err) { console.log(err); - toast({ - title: "Uh oh, something went wrong...", - status: "error", - duration: 3000, - isClosable: true, - }); + toast( + createToast({ + title: "Uh oh, something went wrong...", + status: "error", + duration: 3000, + isClosable: true, + }) + ); return; } }; From 779db736c6aeac7d9d6a638e29b7878182b2cdcd Mon Sep 17 00:00:00 2001 From: Kaylee Wu Date: Fri, 16 Feb 2024 20:13:20 -0500 Subject: [PATCH 11/14] start From d28a4b0b1ecab8427abb374a1d4a2e924daf3c7f Mon Sep 17 00:00:00 2001 From: Kaylee Wu Date: Fri, 16 Feb 2024 20:42:40 -0500 Subject: [PATCH 12/14] remove From c3cd39b88c84aca0d2c80e044321db1810184939 Mon Sep 17 00:00:00 2001 From: Kaylee Wu Date: Wed, 6 Mar 2024 00:42:29 -0500 Subject: [PATCH 13/14] saves reports to database --- apps/backend/src/aws/auth.controller.ts | 53 ++- apps/backend/src/db/frontend/CellTypes.ts | 44 +- apps/backend/src/db/frontend/RowSchema.ts | 88 ++-- .../src/db/timesheets/EntryOperations.ts | 302 +++++++------ .../src/db/timesheets/ItemsOperations.ts | 416 +++++++++--------- .../src/db/timesheets/UploadTimesheet.ts | 122 ++--- apps/frontend/src/aws-exports.js | 35 +- .../TimeCardPage/CellTypes/CommentCell.tsx | 13 +- .../CommentModals/ShowReportModal.tsx | 393 ++++++++++------- .../components/TimeCardPage/TimeTableRow.tsx | 161 ++++--- 10 files changed, 917 insertions(+), 710 deletions(-) diff --git a/apps/backend/src/aws/auth.controller.ts b/apps/backend/src/aws/auth.controller.ts index 52831fb..2711c0b 100644 --- a/apps/backend/src/aws/auth.controller.ts +++ b/apps/backend/src/aws/auth.controller.ts @@ -8,50 +8,49 @@ import { } from "@nestjs/common"; import { AuthService } from "./auth.service"; import { WriteEntryToTable, UserTimesheets } from "../dynamodb"; -import TokenClient from './cognito/cognito.keyparser' -import { TimeSheetSchema } from 'src/db/schemas/Timesheet'; -import * as frontendTimesheetSchemas from 'src/db/schemas/Timesheet' -import { RolesGuard } from 'src/utils/guards/roles.guard'; -import { UploadTimesheet } from 'src/db/timesheets/UploadTimesheet'; -import { TimesheetUpdateRequest } from 'src/db/schemas/UpdateTimesheet'; -import { Formatter } from 'src/db/timesheets/Formatter'; - +import TokenClient from "./cognito/cognito.keyparser"; +import { TimeSheetSchema } from "src/db/schemas/Timesheet"; +import * as frontendTimesheetSchemas from "src/db/schemas/Timesheet"; +import { RolesGuard } from "src/utils/guards/roles.guard"; +import { UploadTimesheet } from "src/db/timesheets/UploadTimesheet"; +import { TimesheetUpdateRequest } from "src/db/schemas/UpdateTimesheet"; +import { Formatter } from "src/db/timesheets/Formatter"; @Controller("auth") @UseGuards(RolesGuard) export class AuthController { - - uploadApi = new UploadTimesheet(); + uploadApi = new UploadTimesheet(); constructor(private authService: AuthService) {} - @Post('timesheet') + @Post("timesheet") public async upload_timesheet( @Headers() headers: any, @Body() body: any ): Promise { - const userId = await TokenClient.grabUserID(headers); + const userId = await TokenClient.grabUserID(headers); if (userId) { - console.log("Update Timesheet Request: Processing") - console.log("Request received:") - console.log(body) - const result = this.uploadApi.updateTimesheet(body, userId); - //TODO: Do something with this result? - return result; + console.log("Update Timesheet Request: Processing"); + console.log("Request received:"); + console.log(body); + console.log(body["Payload"]["Data"]); + const result = this.uploadApi.updateTimesheet(body, userId); + //TODO: Do something with this result? + return result; } } - + @Get("timesheet") //@Roles('breaktime-management-role') - - public async grab_timesheets(@Headers() headers: any): Promise { - const userId = await TokenClient.grabUserID(headers); + public async grab_timesheets( + @Headers() headers: any + ): Promise { + const userId = await TokenClient.grabUserID(headers); if (userId) { - console.log("Fetching timesheets for user ", userId); - return Formatter.fetchUserTimesheets(userId); - - } - return []; + console.log("Fetching timesheets for user ", userId); + return Formatter.fetchUserTimesheets(userId); + } + return []; } } diff --git a/apps/backend/src/db/frontend/CellTypes.ts b/apps/backend/src/db/frontend/CellTypes.ts index a244f55..0d8ab11 100644 --- a/apps/backend/src/db/frontend/CellTypes.ts +++ b/apps/backend/src/db/frontend/CellTypes.ts @@ -2,27 +2,39 @@ // DELETE THIS WHEN MERGED WITH MONOREPO TO DIRECTLY PULL FROM FRONTEND // ////////////////////////////////////////////////////////////////////////// - export enum CellType { - Regular = "Time Worked", - PTO = "PTO" -}; + Regular = "Time Worked", + PTO = "PTO", +} export enum CellStatus { - Active="Active", - Deleted="Deleted" + Active = "Active", + Deleted = "Deleted", +} + +export enum ReportOptions { + Late = "Late Arrival", + LeftEarly = "Early Departure", + Absent = "No Show", } -export enum CommentType { - Comment = "Comment", - Report = "Report", -}; +export enum CommentType { + Comment = "Comment", + Report = "Report", +} export const enum Review_Stages { - UNSUBMITTED = "Not-Submitted", - EMPLOYEE_SUBMITTED = "Employee Submitted", - ADMIN_REVIEW = "Review (Breaktime)", - APPROVED = "Approved" -}; + UNSUBMITTED = "Not-Submitted", + EMPLOYEE_SUBMITTED = "Employee Submitted", + ADMIN_REVIEW = "Review (Breaktime)", + APPROVED = "Approved", +} -export const TABLE_COLUMNS = ['Type', 'Date','Clock-in','Clock-Out','Hours','Comment']; \ No newline at end of file +export const TABLE_COLUMNS = [ + "Type", + "Date", + "Clock-in", + "Clock-Out", + "Hours", + "Comment", +]; diff --git a/apps/backend/src/db/frontend/RowSchema.ts b/apps/backend/src/db/frontend/RowSchema.ts index 8c3c508..962e3e9 100644 --- a/apps/backend/src/db/frontend/RowSchema.ts +++ b/apps/backend/src/db/frontend/RowSchema.ts @@ -3,49 +3,69 @@ ////////////////////////////////////////////////////////////////////////// import { z } from "zod"; -import {CellType, CommentType, Review_Stages, CellStatus} from './CellTypes'; +import { + CellType, + CommentType, + ReportOptions, + Review_Stages, + CellStatus, +} from "./CellTypes"; +const optionalNumber = z.union([z.undefined(), z.number()]); +const optionalString = z.union([z.undefined(), z.string()]); -const optionalNumber = z.union([z.undefined(), z.number()]) -const optionalString = z.union([z.undefined(), z.string()]); - +export const TimeRowEntry = z.union([ + z.undefined(), + z.object({ + Start: optionalNumber, + End: optionalNumber, + AuthorID: optionalString, + }), +]); +export type TimeRowEntry = z.infer; +export const CommentSchema = z.object({ + UUID: z.string(), + AuthorID: z.string(), + Type: z.enum([CommentType.Comment, CommentType.Report]), + Timestamp: z.number(), + Content: z.string(), + State: z.enum([CellStatus.Active, CellStatus.Deleted]), +}); -export const TimeRowEntry = z.union([z.undefined(), z.object({ - Start: optionalNumber, End: optionalNumber, AuthorID: optionalString -})]); -export type TimeRowEntry = z.infer +export type CommentSchema = z.infer; -export const CommentSchema = z.object({ - UUID: z.string(), - AuthorID:z.string(), - Type: z.enum([CommentType.Comment, CommentType.Report]), - Timestamp: z.number(), - Content: z.string(), - State: z.enum([CellStatus.Active, CellStatus.Deleted]), -}); +export const ReportSchema = z.object({ + AuthorID: z.string(), + Timestamp: z.number(), + Type: z.nativeEnum(CommentType), + CorrectTime: z.number(), + Content: z.nativeEnum(ReportOptions), + Notified: z.string(), + Explanation: z.string(), + State: z.nativeEnum(CellStatus), +}); -export type CommentSchema = z.infer +export type ReportSchema = z.infer; -export const RowType = z.enum([CellType.Regular, CellType.PTO]); -export type RowType = z.infer +export const RowType = z.enum([CellType.Regular, CellType.PTO]); +export type RowType = z.infer; export const RowSchema = z.object({ - UUID: z.string(), - Type: RowType, - Date: z.number(), - Associate: TimeRowEntry, - Supervisor: TimeRowEntry, - Admin: TimeRowEntry, - Comment: z.union([z.undefined(), z.array(CommentSchema)]) -}); -export type RowSchema = z.infer - + Type: z.nativeEnum(CellType), + UUID: z.string(), + Date: z.number(), + Associate: TimeRowEntry, + Supervisor: TimeRowEntry, + Admin: TimeRowEntry, + Comment: z.union([z.undefined(), z.array(CommentSchema || ReportSchema)]), +}); +export type RowSchema = z.infer; export const ScheduledRowSchema = z.object({ - UUID: z.string(), - Date: z.number(), - Entry: TimeRowEntry -}); + UUID: z.string(), + Date: z.number(), + Entry: TimeRowEntry, +}); -export type ScheduledRowSchema = z.infer +export type ScheduledRowSchema = z.infer; diff --git a/apps/backend/src/db/timesheets/EntryOperations.ts b/apps/backend/src/db/timesheets/EntryOperations.ts index f96c903..0cced00 100644 --- a/apps/backend/src/db/timesheets/EntryOperations.ts +++ b/apps/backend/src/db/timesheets/EntryOperations.ts @@ -1,156 +1,212 @@ -import * as frontendRowTypes from '../frontend/RowSchema' -import * as dbTimesheetTypes from '../schemas/Timesheet' -import * as requestSchemas from '../schemas/UpdateTimesheet' -import {FrontendTimeSheetSchema} from '../frontend/TimesheetSchema' -import * as frontendTypes from '../frontend/CellTypes' +import * as frontendRowTypes from "../frontend/RowSchema"; +import * as dbTimesheetTypes from "../schemas/Timesheet"; +import * as requestSchemas from "../schemas/UpdateTimesheet"; +import { FrontendTimeSheetSchema } from "../frontend/TimesheetSchema"; +import * as frontendTypes from "../frontend/CellTypes"; +import { User } from "src/utils/decorators/user.decorator"; +import { CustomSMSSenderLambdaVersionType } from "@aws-sdk/client-cognito-identity-provider"; /* Code for converting from the frontend to our backend equivalents. Useful for actually processing this to be stored in our database / align with what our backend expects to see in this data. */ - // Class to represent the mappings from a frontend key to a backend key alongside a conversion fn for the data -class KeyPairMappings { - originalKey:string; - conversionFn: Function; - finalKey: string; +class KeyPairMappings { + originalKey: string; + conversionFn: Function; + finalKey: string; - constructor(originalKey:string, finalKey: string, conversionFn:Function) { - this.originalKey = originalKey; - this.conversionFn = conversionFn; - this.finalKey = finalKey; - } + constructor(originalKey: string, finalKey: string, conversionFn: Function) { + this.originalKey = originalKey; + this.conversionFn = conversionFn; + this.finalKey = finalKey; + } } export class frontendEntryConversions { - //NOTE: The key in the dictionary must match frontend key name as this is how we automatically convert keys - private static hoursDataMappings = { - Type: new KeyPairMappings("Type", "Type", frontendEntryConversions.toDBType), - Associate: new KeyPairMappings("Associate", "AssociateTimes", frontendEntryConversions.toDBRowEntry), - Supervisor: new KeyPairMappings("Supervisor", "SupervisorTimes", frontendEntryConversions.toDBRowEntry), - Admin: new KeyPairMappings("Admin", "AdminTimes", frontendEntryConversions.toDBRowEntry), - Comment: new KeyPairMappings("Comment", "Note", frontendEntryConversions.toDBNotes) - } - + //NOTE: The key in the dictionary must match frontend key name as this is how we automatically convert keys + private static hoursDataMappings = { + Type: new KeyPairMappings( + "Type", + "Type", + frontendEntryConversions.toDBType + ), + Associate: new KeyPairMappings( + "Associate", + "AssociateTimes", + frontendEntryConversions.toDBRowEntry + ), + Supervisor: new KeyPairMappings( + "Supervisor", + "SupervisorTimes", + frontendEntryConversions.toDBRowEntry + ), + Admin: new KeyPairMappings( + "Admin", + "AdminTimes", + frontendEntryConversions.toDBRowEntry + ), + Comment: new KeyPairMappings( + "Comment", + "Note", + frontendEntryConversions.toDBNotes + ), + }; - /* + /* Delegate that converts the item we are inserting to its database equivalent so that it can actually exist on the table */ - public static insertConversion(body: requestSchemas.InsertRequest) : requestSchemas.InsertRequest { - - switch (body.Type) { - case requestSchemas.TimesheetListItems.TABLEDATA: - return { - ...body, - Item: this.toDBRow(frontendRowTypes.RowSchema.parse(body.Item)) - - } - case requestSchemas.TimesheetListItems.SCHEDULEDATA: - throw new Error("Not yet implemented") + public static insertConversion( + body: requestSchemas.InsertRequest + ): requestSchemas.InsertRequest { + switch (body.Type) { + case requestSchemas.TimesheetListItems.TABLEDATA: + return { + ...body, + Item: this.toDBRow(frontendRowTypes.RowSchema.parse(body.Item)), + }; + case requestSchemas.TimesheetListItems.SCHEDULEDATA: + throw new Error("Not yet implemented"); - case requestSchemas.TimesheetListItems.WEEKNOTES: - throw new Error("Not yet implemented") + case requestSchemas.TimesheetListItems.WEEKNOTES: + throw new Error("Not yet implemented"); - default: - throw new Error("Invalid conversion type provided"); - } + default: + throw new Error("Invalid conversion type provided"); } + } - /* + /* Delegate that converts the item we are updating to its database equivalent so that it can actually exist on the table */ - public static updateConversion(body: requestSchemas.UpdateRequest) : requestSchemas.UpdateRequest { - switch (body.Type) { - case requestSchemas.TimesheetListItems.TABLEDATA: + public static updateConversion( + body: requestSchemas.UpdateRequest + ): requestSchemas.UpdateRequest { + switch (body.Type) { + case requestSchemas.TimesheetListItems.TABLEDATA: + const convertedKey = this.hoursDataMappings[body.Attribute].finalKey; + console.log(convertedKey); + const convertedValue = this.hoursDataMappings[ + body.Attribute + ].conversionFn(body.Data); + console.log(convertedValue); + return { + ...body, + Attribute: convertedKey, + Data: convertedValue, + }; + case requestSchemas.TimesheetListItems.SCHEDULEDATA: + throw new Error("Not yet implemented"); + case requestSchemas.TimesheetListItems.WEEKNOTES: + throw new Error("Not yet implemented"); + default: + throw new Error("Invalid conversion type provided"); + } + } - const convertedKey = this.hoursDataMappings[body.Attribute].finalKey; - const convertedValue = this.hoursDataMappings[body.Attribute].conversionFn(body.Data) - return { - ...body, - Attribute: convertedKey, - Data: convertedValue - } - case requestSchemas.TimesheetListItems.SCHEDULEDATA: - throw new Error("Not yet implemented") - case requestSchemas.TimesheetListItems.WEEKNOTES: - throw new Error("Not yet implemented") - default: - throw new Error("Invalid conversion type provided"); - } - } - - /* + /* Converts a row in our timesheet to our database equivalent from frontend. */ - private static toDBRow(row: frontendRowTypes.RowSchema): dbTimesheetTypes.TimesheetEntrySchema { - return dbTimesheetTypes.TimesheetEntrySchema.parse({ - Type: this.toDBType(row.Type), - EntryID: row.UUID, - Date: row.Date, - AssociateTimes: this.toDBRowEntry(row.Associate), - SupervisorTimes: this.toDBRowEntry(row.Supervisor), - AdminTimes: this.toDBRowEntry(row.Admin), - Note: row.Comment?.map((comment) => this.toDBNote(comment)) - }); - } + private static toDBRow( + row: frontendRowTypes.RowSchema + ): dbTimesheetTypes.TimesheetEntrySchema { + return dbTimesheetTypes.TimesheetEntrySchema.parse({ + Type: this.toDBType(row.Type), + EntryID: row.UUID, + Date: row.Date, + AssociateTimes: this.toDBRowEntry(row.Associate), + SupervisorTimes: this.toDBRowEntry(row.Supervisor), + AdminTimes: this.toDBRowEntry(row.Admin), + Note: row.Comment?.map((comment) => this.toDBNote(comment)), + }); + } - // Converts a timesheet entry to our database equivalent from frontend. - private static toDBRowEntry(row: frontendRowTypes.TimeRowEntry | undefined): dbTimesheetTypes.TimeEntrySchema | undefined{ - if (row !== undefined) { - return dbTimesheetTypes.TimeEntrySchema.parse({ - StartDateTime: row.Start, - EndDateTime: row.End, - AuthorUUID: row.AuthorID - }); - } - return undefined; + // Converts a timesheet entry to our database equivalent from frontend. + private static toDBRowEntry( + row: frontendRowTypes.TimeRowEntry | undefined + ): dbTimesheetTypes.TimeEntrySchema | undefined { + if (row !== undefined) { + return dbTimesheetTypes.TimeEntrySchema.parse({ + StartDateTime: row.Start, + EndDateTime: row.End, + AuthorUUID: row.AuthorID, + }); } + return undefined; + } - // Converts a frontend cell type to our database equivalent. - private static toDBType(entryType: frontendRowTypes.RowType): dbTimesheetTypes.CellType { - switch (entryType) { - case frontendTypes.CellType.Regular: - return dbTimesheetTypes.CellType.REGULAR; - case frontendTypes.CellType.PTO: - return dbTimesheetTypes.CellType.PTO; - default: - return undefined - } + // Converts a frontend cell type to our database equivalent. + private static toDBType( + entryType: frontendRowTypes.RowType + ): dbTimesheetTypes.CellType { + switch (entryType) { + case frontendTypes.CellType.Regular: + return dbTimesheetTypes.CellType.REGULAR; + case frontendTypes.CellType.PTO: + return dbTimesheetTypes.CellType.PTO; + default: + return undefined; } + } - // Converts from our frontend week comments to our database equivalents. - private static toDBNotes(comments: frontendRowTypes.CommentSchema[] | undefined): dbTimesheetTypes.NoteSchema[] | undefined { - if (comments !== undefined) { - return comments.map((comment) => frontendEntryConversions.toDBNote(comment)) - - } - return undefined; + // Converts from our frontend week comments to our database equivalents. + private static toDBNotes( + comments: frontendRowTypes.CommentSchema[] | undefined + ): dbTimesheetTypes.NoteSchema[] | undefined { + if (comments !== undefined) { + return comments.map((comment) => + frontendEntryConversions.toDBNote(comment) + ); } + return undefined; + } - // Converts a singular week comment / note from our frontend to database. - private static toDBNote(comment: frontendRowTypes.CommentSchema | undefined): dbTimesheetTypes.NoteSchema | undefined { - if (comment !== undefined) { - return dbTimesheetTypes.NoteSchema.parse({ - Type: comment.Type, - EntryID: comment.UUID, - AuthorUUID: comment.AuthorID, - DateTime: comment.Timestamp, - Content: comment.Content, - State: comment.State - }); - } - return undefined; + // Converts a singular week comment / note from our frontend to database. + private static toDBNote( + note: + | frontendRowTypes.CommentSchema + | frontendRowTypes.ReportSchema + | undefined + ): dbTimesheetTypes.NoteSchema | undefined { + if (note === undefined) { + return note; } - // Converts from a singular frontend schedule entry to our database equivalent. - private static toDBSchedule(row: frontendRowTypes.ScheduledRowSchema): dbTimesheetTypes.ScheduleEntrySchema { - return dbTimesheetTypes.ScheduleEntrySchema.parse({ - EntryID: row.UUID, - Date: row.Date, - StartDateTime: row.Entry?.Start, - EndDateTime: row.Entry?.End, - AuthorUUID: row.Entry?.AuthorID - }) + if (note.Type === frontendTypes.CommentType["Comment"]) { + const comment = note as frontendRowTypes.CommentSchema; + return dbTimesheetTypes.NoteSchema.parse({ + Type: comment.Type, + EntryID: comment.UUID, + AuthorUUID: comment.AuthorID, + DateTime: comment.Timestamp, + Content: comment.Content, + State: comment.State, + }); + } else if (note.Type === frontendTypes.CommentType["Report"]) { + const report = note as frontendRowTypes.ReportSchema; + return dbTimesheetTypes.NoteSchema.parse({ + Type: report.Type, + EntryID: "", + AuthorUUID: report.AuthorID, + DateTime: 2, + Content: `${report.Content},${report.Notified},${report.Explanation}`, + State: report.State, + }); } -} \ No newline at end of file + return undefined; + } + + // Converts from a singular frontend schedule entry to our database equivalent. + private static toDBSchedule( + row: frontendRowTypes.ScheduledRowSchema + ): dbTimesheetTypes.ScheduleEntrySchema { + return dbTimesheetTypes.ScheduleEntrySchema.parse({ + EntryID: row.UUID, + Date: row.Date, + StartDateTime: row.Entry?.Start, + EndDateTime: row.Entry?.End, + AuthorUUID: row.Entry?.AuthorID, + }); + } +} diff --git a/apps/backend/src/db/timesheets/ItemsOperations.ts b/apps/backend/src/db/timesheets/ItemsOperations.ts index 4ba7705..602527e 100644 --- a/apps/backend/src/db/timesheets/ItemsOperations.ts +++ b/apps/backend/src/db/timesheets/ItemsOperations.ts @@ -1,21 +1,36 @@ -import {TimeSheetSchema, TimesheetEntrySchema, ScheduleEntrySchema, NoteSchema, StatusEntryType} from '../schemas/Timesheet' -import {UpdateRequest, InsertRequest, DeleteRequest, TimesheetListItems, StatusChangeRequest} from '../schemas/UpdateTimesheet' -import { ExceptionsHandler } from '@nestjs/core/exceptions/exceptions-handler'; -//Not sure why but only works if imported like this :| -const moment = require('moment-timezone'); +import { + TimeSheetSchema, + TimesheetEntrySchema, + ScheduleEntrySchema, + NoteSchema, + StatusEntryType, +} from "../schemas/Timesheet"; +import { + UpdateRequest, + InsertRequest, + DeleteRequest, + TimesheetListItems, + StatusChangeRequest, +} from "../schemas/UpdateTimesheet"; +import { ExceptionsHandler } from "@nestjs/core/exceptions/exceptions-handler"; +//Not sure why but only works if imported like this :| +const moment = require("moment-timezone"); /* Interface holding all operations available for a field in the timesheet that should support updates, inserts, and deletions. */ interface ItemsOperations { - // Insert into the list of items - Insert(timesheet: TimeSheetSchema, body:InsertRequest): TimeSheetSchema - // Delete a specific item from the list of items - Delete(timesheet: TimeSheetSchema, body:DeleteRequest): TimeSheetSchema - // Update a specific item in the list of items - Update(timesheet: TimeSheetSchema, body:UpdateRequest) : TimeSheetSchema - // TODO: add a new StatusChange(....) function - StatusChange(timesheet: TimeSheetSchema, body:StatusChangeRequest): TimeSheetSchema + // Insert into the list of items + Insert(timesheet: TimeSheetSchema, body: InsertRequest): TimeSheetSchema; + // Delete a specific item from the list of items + Delete(timesheet: TimeSheetSchema, body: DeleteRequest): TimeSheetSchema; + // Update a specific item in the list of items + Update(timesheet: TimeSheetSchema, body: UpdateRequest): TimeSheetSchema; + // TODO: add a new StatusChange(....) function + StatusChange( + timesheet: TimeSheetSchema, + body: StatusChangeRequest + ): TimeSheetSchema; } /* @@ -23,106 +38,99 @@ interface ItemsOperations { I.e. when we want to update a piece of the table data, go to the implementation of this interface above for table data. */ export class ItemsDelegator { - // Class to determine what field of the timesheet we are performing item operations on - tableData = new HoursDataOperations() - scheduleData = new ScheduledDataOperations() - notesData = new NotesOperations() - + // Class to determine what field of the timesheet we are performing item operations on + tableData = new HoursDataOperations(); + scheduleData = new ScheduledDataOperations(); + notesData = new NotesOperations(); - public AttributeToModify(body: InsertRequest | DeleteRequest | UpdateRequest) { - switch (body.Type) { - case TimesheetListItems.TABLEDATA: - return this.tableData; - case TimesheetListItems.SCHEDULEDATA: - return this.scheduleData; - case TimesheetListItems.WEEKNOTES: - return this.notesData; - default: - throw new Error ("Invalid operation provided"); - } + public AttributeToModify( + body: InsertRequest | DeleteRequest | UpdateRequest + ) { + switch (body.Type) { + case TimesheetListItems.TABLEDATA: + return this.tableData; + case TimesheetListItems.SCHEDULEDATA: + return this.scheduleData; + case TimesheetListItems.WEEKNOTES: + return this.notesData; + default: + throw new Error("Invalid operation provided"); } + } } - /* Implementation for processing operations on the TableData (HoursData) of our timesheet i.e. the user entered rows of the time they worked. */ export class HoursDataOperations implements ItemsOperations { - public Insert(timesheet: TimeSheetSchema, body:InsertRequest) { - const data = timesheet.HoursData; + public Insert(timesheet: TimeSheetSchema, body: InsertRequest) { + const data = timesheet.HoursData; - const item = TimesheetEntrySchema.parse(body.Item); - // Sorting is currently only day by day based - need some way of minute by minute - var idx = 0; - for (idx; idx < data.length; idx += 1) { - const row = data[idx]; - if (moment.unix(row.Date).isAfter(moment.unix(item.Date), 'day')) { - break; - } - } - //Insert into front of list - if (idx === 0) { - return { - ...timesheet, - HoursData: [ - item, - ...data - ] - }; - } else if (idx === data.length) { - //End of list - return { - ...timesheet, - HoursData: [ - ...data, - item - ] - }; - } else { - return { - ...timesheet, - HoursData: [ - ...data.slice(0, idx), - item, - ...data.slice(idx + 1 ) - ] - } - } + const item = TimesheetEntrySchema.parse(body.Item); + // Sorting is currently only day by day based - need some way of minute by minute + var idx = 0; + for (idx; idx < data.length; idx += 1) { + const row = data[idx]; + if (moment.unix(row.Date).isAfter(moment.unix(item.Date), "day")) { + break; + } } - public Delete(timesheet: TimeSheetSchema, body:DeleteRequest) { - return { - ...timesheet, - HoursData: timesheet.HoursData.filter((row) => row.EntryID !== body.Id) - }; - + //Insert into front of list + if (idx === 0) { + return { + ...timesheet, + HoursData: [item, ...data], + }; + } else if (idx === data.length) { + //End of list + return { + ...timesheet, + HoursData: [...data, item], + }; + } else { + return { + ...timesheet, + HoursData: [...data.slice(0, idx), item, ...data.slice(idx + 1)], + }; } + } + public Delete(timesheet: TimeSheetSchema, body: DeleteRequest) { + return { + ...timesheet, + HoursData: timesheet.HoursData.filter((row) => row.EntryID !== body.Id), + }; + } - public Update(timesheet: TimeSheetSchema, body:UpdateRequest) { - if (timesheet.HoursData?.filter((row) => row.EntryID === body.Id).length === 0) { - throw new Error("Could not find a row with that ID"); - } - return { - ...timesheet, - HoursData: timesheet.HoursData.map((row) => { - // Only update the one specific id - if (row.EntryID === body.Id) { - return { - ...row, - [body.Attribute] : body.Data - }; - } - return row; - }) - } + public Update(timesheet: TimeSheetSchema, body: UpdateRequest) { + if ( + timesheet.HoursData?.filter((row) => row.EntryID === body.Id).length === 0 + ) { + throw new Error("Could not find a row with that ID"); } - - public StatusChange(timesheet: TimeSheetSchema, body:StatusChangeRequest) { - if (timesheet.TimesheetID !== body.TimesheetId) { - throw new Error("Requested timesheet does not match timesheet ID of timesheet being updated"); + return { + ...timesheet, + HoursData: timesheet.HoursData.map((row) => { + // Only update the one specific id + if (row.EntryID === body.Id) { + return { + ...row, + [body.Attribute]: body.Data, + }; } + return row; + }), + }; + } + + public StatusChange(timesheet: TimeSheetSchema, body: StatusChangeRequest) { + if (timesheet.TimesheetID !== body.TimesheetId) { + throw new Error( + "Requested timesheet does not match timesheet ID of timesheet being updated" + ); + } - /* + /* As an example... original Status : {HoursSubmitted=undefined, HoursReviewed=undefined, Finalized=undefined} @@ -131,130 +139,122 @@ export class HoursDataOperations implements ItemsOperations { new Status: {HoursSubmitted={Date: 0638457, AuthorID: 123456}, HoursReviewed=undefined, Finalized=undefined} */ - const newStatusEntry = {Date: body.dateSubmitted, AuthorID: body.authorId} - const updatedStatus = { - ...timesheet.Status, - [body.statusType.valueOf()]: newStatusEntry - } + const newStatusEntry = { + Date: body.dateSubmitted, + AuthorID: body.authorId, + }; + const updatedStatus = { + ...timesheet.Status, + [body.statusType.valueOf()]: newStatusEntry, + }; - console.log("Handling Status Change Request for timesheet %s", body.TimesheetId.valueOf()) - console.log("New Status Object:\n %s", updatedStatus) - return { - ...timesheet, - Status: updatedStatus - } - } + console.log( + "Handling Status Change Request for timesheet %s", + body.TimesheetId.valueOf() + ); + console.log("New Status Object:\n %s", updatedStatus); + return { + ...timesheet, + Status: updatedStatus, + }; + } } -// Class for operations on the schedule data field - i.e. the supervisor reported hours they should have worked. +// Class for operations on the schedule data field - i.e. the supervisor reported hours they should have worked. export class ScheduledDataOperations implements ItemsOperations { - public Insert(timesheet: TimeSheetSchema, body:InsertRequest) { - const data = timesheet.ScheduleData; - const item = ScheduleEntrySchema.parse(body.Item); - //TODO - Fledge out the sorting to be simplified / actually accurate on the minute by minute. Currently is only based on day - var idx = 0; - for (idx; idx < data.length; idx += 1) { - const row = data[idx]; - if (moment.unix(row.Date).isAfter(moment.unix(item.Date), 'day')) { - break; - } - } - //Insert into front of list - if (idx === 0) { - return { - ...timesheet, - ScheduleData: [ - item, - ...data - ] - }; - } else if (idx === data.length) { - //End of list - return { - ...timesheet, - ScheduleData: [ - ...data, - item - ] - }; - } else { - return { - ...timesheet, - ScheduleData: [ - ...data.slice(0, idx), - item, - ...data.slice(idx + 1 ) - ] - } - } + public Insert(timesheet: TimeSheetSchema, body: InsertRequest) { + const data = timesheet.ScheduleData; + const item = ScheduleEntrySchema.parse(body.Item); + //TODO - Fledge out the sorting to be simplified / actually accurate on the minute by minute. Currently is only based on day + var idx = 0; + for (idx; idx < data.length; idx += 1) { + const row = data[idx]; + if (moment.unix(row.Date).isAfter(moment.unix(item.Date), "day")) { + break; + } } - public Delete(timesheet: TimeSheetSchema, body:DeleteRequest) { - return { - ...timesheet, - ScheduleData: timesheet.ScheduleData.filter((row) => row.EntryID !== body.Id) - } + //Insert into front of list + if (idx === 0) { + return { + ...timesheet, + ScheduleData: [item, ...data], + }; + } else if (idx === data.length) { + //End of list + return { + ...timesheet, + ScheduleData: [...data, item], + }; + } else { + return { + ...timesheet, + ScheduleData: [...data.slice(0, idx), item, ...data.slice(idx + 1)], + }; } + } + public Delete(timesheet: TimeSheetSchema, body: DeleteRequest) { + return { + ...timesheet, + ScheduleData: timesheet.ScheduleData.filter( + (row) => row.EntryID !== body.Id + ), + }; + } - public Update(timesheet: TimeSheetSchema, body:UpdateRequest) { - //TODO - Add in functionality to trigger insert instead of update if ID does not yet exist - return { - ...timesheet, - ScheduleData: timesheet.ScheduleData.map((row) => { - // Only update the one specific id - if (row.EntryID === body.Id) { - return { - ...row, - [body.Attribute] : body.Data - }; - } - return row; - }) + public Update(timesheet: TimeSheetSchema, body: UpdateRequest) { + //TODO - Add in functionality to trigger insert instead of update if ID does not yet exist + return { + ...timesheet, + ScheduleData: timesheet.ScheduleData.map((row) => { + // Only update the one specific id + if (row.EntryID === body.Id) { + return { + ...row, + [body.Attribute]: body.Data, + }; } - } + return row; + }), + }; + } - public StatusChange(timesheet: TimeSheetSchema, body:StatusChangeRequest) { - return undefined; - } + public StatusChange(timesheet: TimeSheetSchema, body: StatusChangeRequest) { + return undefined; + } } -// Operations on the weekly notes on the timesheet - i.e. comments relating to the entire timesheet / specific day worked. +// Operations on the weekly notes on the timesheet - i.e. comments relating to the entire timesheet / specific day worked. export class NotesOperations implements ItemsOperations { - public Insert(timesheet: TimeSheetSchema, body:InsertRequest) { - return { - ...timesheet, - WeekNotes: [ - ...timesheet.WeekNotes, - NoteSchema.parse(body.Item) - ] - }; - } - public Delete(timesheet: TimeSheetSchema, body:DeleteRequest) { - return { - ...timesheet, - WeekNotes: timesheet.WeekNotes.filter((note) => note.EntryID !== body.Id) - } - } - - public Update(timesheet: TimeSheetSchema, body:UpdateRequest) { - //TODO - Add in functionality to trigger insert instead of update if ID does not yet exist + public Insert(timesheet: TimeSheetSchema, body: InsertRequest) { + return { + ...timesheet, + WeekNotes: [...timesheet.WeekNotes, NoteSchema.parse(body.Item)], + }; + } + public Delete(timesheet: TimeSheetSchema, body: DeleteRequest) { + return { + ...timesheet, + WeekNotes: timesheet.WeekNotes.filter((note) => note.EntryID !== body.Id), + }; + } - return { - ...timesheet, - WeekNotes: timesheet.WeekNotes.map((note) => { - if (note.EntryID === body.Id) { - return { - ...note, - [body.Attribute] : body.Data - } - } - return note ; - }) + public Update(timesheet: TimeSheetSchema, body: UpdateRequest) { + //TODO - Add in functionality to trigger insert instead of update if ID does not yet exist + return { + ...timesheet, + WeekNotes: timesheet.WeekNotes.map((note) => { + if (note.EntryID === body.Id) { + return { + ...note, + [body.Attribute]: body.Data, + }; } - } + return note; + }), + }; + } - public StatusChange(timesheet: TimeSheetSchema, body:StatusChangeRequest) { - return undefined; - } + public StatusChange(timesheet: TimeSheetSchema, body: StatusChangeRequest) { + return undefined; + } } - - \ No newline at end of file diff --git a/apps/backend/src/db/timesheets/UploadTimesheet.ts b/apps/backend/src/db/timesheets/UploadTimesheet.ts index 7917881..ca5ccd9 100644 --- a/apps/backend/src/db/timesheets/UploadTimesheet.ts +++ b/apps/backend/src/db/timesheets/UploadTimesheet.ts @@ -1,65 +1,89 @@ import { DBToFrontend } from "./FrontendConversions"; import { FrontendTimeSheetSchema } from "../frontend/TimesheetSchema"; -import { TimesheetUpdateRequest, TimesheetOperations } from "../schemas/UpdateTimesheet"; +import { + TimesheetUpdateRequest, + TimesheetOperations, +} from "../schemas/UpdateTimesheet"; -import {UserTimesheets} from "src/dynamodb" +import { UserTimesheets } from "src/dynamodb"; import { ExceptionsHandler } from "@nestjs/core/exceptions/exceptions-handler"; import { HoursDataOperations, ItemsDelegator } from "./ItemsOperations"; -import {WriteEntryToTable} from "src/dynamodb" -import {frontendEntryConversions} from './EntryOperations' +import { WriteEntryToTable } from "src/dynamodb"; +import { frontendEntryConversions } from "./EntryOperations"; export class UploadTimesheet { - - private delegator = new ItemsDelegator() + private delegator = new ItemsDelegator(); - public async updateTimesheet(request: TimesheetUpdateRequest, userid: string): Promise { - /* + public async updateTimesheet( + request: TimesheetUpdateRequest, + userid: string + ): Promise { + /* Provided a request to update a timesheet, processes the request and then return a response indicating success or failure. request: The request we are processing userid: The user we are processing this for */ - //Retrieve a specified timesheet - console.log(request) - const userTimesheets = await UserTimesheets(userid); - const selectedTimesheet = userTimesheets.filter((timesheet) => timesheet.TimesheetID === request.TimesheetID) - if (selectedTimesheet.length == 1) { - console.log("Timesheet found for Update Timesheet Operation %s", request.Operation.valueOf()) - var modifiedTimesheet = undefined; - switch (request.Operation) { - case TimesheetOperations.STATUS_CHANGE: - // This operation should only be supported for Hours Data - modifiedTimesheet = this.delegator.tableData.StatusChange(selectedTimesheet[0], request.Payload); - break; - case TimesheetOperations.DELETE: - modifiedTimesheet = this.delegator.AttributeToModify(request.Payload).Delete(selectedTimesheet[0], request.Payload); - break; - case TimesheetOperations.INSERT: - //Determine attribute we are modifying and then also convert the field from frontend to backend. - modifiedTimesheet = this.delegator.AttributeToModify(request.Payload).Insert(selectedTimesheet[0], - frontendEntryConversions.insertConversion(request.Payload)); - break; - case TimesheetOperations.UPDATE: - //Determine attribute we are modifying and then also convert the field from frontend to backend. - modifiedTimesheet = this.delegator.AttributeToModify(request.Payload).Update(selectedTimesheet[0], - frontendEntryConversions.updateConversion(request.Payload)); - - break; - default: - throw new Error(`Invalid operation: ${request.Operation}`); - } - if (modifiedTimesheet !== undefined) { - WriteEntryToTable(modifiedTimesheet); - return "Success :)" - } - return "Failure"; - } else if (selectedTimesheet.length > 1) { - throw new Error("Multiple timesheets have the same timesheet ID for this user"); - } else { - return "Error! No Timesheet with that ID!" - // throw new Error("No Timesheet with that ID"); - } + //Retrieve a specified timesheet + console.log(request); + const userTimesheets = await UserTimesheets(userid); + const selectedTimesheet = userTimesheets.filter( + (timesheet) => timesheet.TimesheetID === request.TimesheetID + ); + if (selectedTimesheet.length == 1) { + console.log( + "Timesheet found for Update Timesheet Operation %s", + request.Operation.valueOf() + ); + var modifiedTimesheet = undefined; + switch (request.Operation) { + case TimesheetOperations.STATUS_CHANGE: + // This operation should only be supported for Hours Data + modifiedTimesheet = this.delegator.tableData.StatusChange( + selectedTimesheet[0], + request.Payload + ); + break; + case TimesheetOperations.DELETE: + modifiedTimesheet = this.delegator + .AttributeToModify(request.Payload) + .Delete(selectedTimesheet[0], request.Payload); + break; + case TimesheetOperations.INSERT: + //Determine attribute we are modifying and then also convert the field from frontend to backend. + modifiedTimesheet = this.delegator + .AttributeToModify(request.Payload) + .Insert( + selectedTimesheet[0], + frontendEntryConversions.insertConversion(request.Payload) + ); + break; + case TimesheetOperations.UPDATE: + //Determine attribute we are modifying and then also convert the field from frontend to backend. + modifiedTimesheet = this.delegator + .AttributeToModify(request.Payload) + .Update( + selectedTimesheet[0], + frontendEntryConversions.updateConversion(request.Payload) + ); + break; + default: + throw new Error(`Invalid operation: ${request.Operation}`); + } + if (modifiedTimesheet !== undefined) { + WriteEntryToTable(modifiedTimesheet); + return "Success :)"; + } + return "Failure"; + } else if (selectedTimesheet.length > 1) { + throw new Error( + "Multiple timesheets have the same timesheet ID for this user" + ); + } else { + return "Error! No Timesheet with that ID!"; + // throw new Error("No Timesheet with that ID"); } -} \ No newline at end of file + } +} diff --git a/apps/frontend/src/aws-exports.js b/apps/frontend/src/aws-exports.js index 8e37d9d..ba025d5 100644 --- a/apps/frontend/src/aws-exports.js +++ b/apps/frontend/src/aws-exports.js @@ -2,24 +2,23 @@ // WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten. const awsmobile = { - aws_project_region: 'us-east-1', - // aws_cognito_identity_pool_id: 'us-east-2:f601ad7b-b9fc-4e4a-9163-9d12d393fd7f', - // aws_cognito_identity_pool_id: '', + aws_project_region: "us-east-1", + // aws_cognito_identity_pool_id: 'us-east-2:f601ad7b-b9fc-4e4a-9163-9d12d393fd7f', + // aws_cognito_identity_pool_id: '', - aws_cognito_region: 'us-east-1', - // aws_user_pools_id: 'us-east-2_zG2SfHpXC', - aws_user_pools_id: 'us-east-1_BO2rSGqTd', - - aws_user_pools_web_client_id: 'duquj0kkirg431tdu9ketn7vp', - oauth: {}, - aws_cognito_login_mechanism: [], - aws_cognito_signup_attributes: ['EMAIL'], - aws_cognito_mfa_configuration: 'OFF', - aws_cognito_mfa_types: ['SMS'], - aws_cognito_password_protection_settings: { - passwordPolicyMinLength: 8, - passwordPolicyCharacters: [], - }, + aws_cognito_region: "us-east-1", + // aws_user_pools_id: 'us-east-2_zG2SfHpXC', + aws_user_pools_id: "us-east-1_BO2rSGqTd", + aws_user_pools_web_client_id: "3cekddbq7ail1s50qt2thfle7u", + oauth: {}, + aws_cognito_login_mechanism: [], + aws_cognito_signup_attributes: ["EMAIL"], + aws_cognito_mfa_configuration: "OFF", + aws_cognito_mfa_types: ["SMS"], + aws_cognito_password_protection_settings: { + passwordPolicyMinLength: 8, + passwordPolicyCharacters: [], + }, }; -export default awsmobile; \ No newline at end of file +export default awsmobile; diff --git a/apps/frontend/src/components/TimeCardPage/CellTypes/CommentCell.tsx b/apps/frontend/src/components/TimeCardPage/CellTypes/CommentCell.tsx index 59eb993..28d66a4 100644 --- a/apps/frontend/src/components/TimeCardPage/CellTypes/CommentCell.tsx +++ b/apps/frontend/src/components/TimeCardPage/CellTypes/CommentCell.tsx @@ -12,13 +12,15 @@ import ShowReportModal from "./CommentModals/ShowReportModal"; interface CommentProps { comments: CommentSchema[] | undefined; date: number; + updateComments: Function; timesheetID: number; } export function CommentCell({ comments, date, - timesheetID + updateComments, + timesheetID, }: CommentProps) { const [currentComments, setCurrentComments] = useState( getAllActiveCommentsOfType(CommentType.Comment, comments) @@ -36,8 +38,13 @@ export function CommentCell({ } }, [user?.Type]); + const updateReports = (updatedReports: ReportSchema[]) => { + setReports(updatedReports); + updateComments("Comment", currentComments.concat(updatedReports)); + }; + return ( - + { + setReports: Function, + comments: ReportSchema[], + prevComment: ReportSchema, + newComment: ReportSchema +) => { // previous comment edited over so set it to deleted - prevComment.State = CellStatus.Deleted - setReports(getAllActiveCommentsOfType(CommentType.Report, [...comments, newComment]) as ReportSchema[]); + prevComment.State = CellStatus.Deleted; + setReports( + getAllActiveCommentsOfType(CommentType.Report, [ + ...comments, + newComment, + ]) as ReportSchema[] + ); // TODO: save to DB }; const deleteComment = ( - onCloseDisplay: Function, - setComments: Function, - comments: CommentSchema[], - typeOfComment: CommentType, - comment: CommentSchema) => { + onCloseDisplay: Function, + setComments: Function, + comments: CommentSchema[], + typeOfComment: CommentType, + comment: CommentSchema +) => { // TODO: add confirmation popup - comment.State = CellStatus.Deleted + comment.State = CellStatus.Deleted; setComments(getAllActiveCommentsOfType(typeOfComment, comments)); if (comments.length === 1) { - onCloseDisplay() + onCloseDisplay(); } // TODO: save to DB -} +}; interface ShowReportModalProps { date: number; @@ -82,159 +83,218 @@ export default function ShowReportModal({ reports, setReports, isEditable, - timesheetID + timesheetID, }: ShowReportModalProps) { - const { isOpen: isOpenDisplay, onOpen: onOpenDisplay, onClose: onCloseDisplay } = useDisclosure(); - const { isOpen: isOpenAdd, onOpen: onOpenAdd, onClose: onCloseAdd } = useDisclosure(); + const { + isOpen: isOpenDisplay, + onOpen: onOpenDisplay, + onClose: onCloseDisplay, + } = useDisclosure(); + const { + isOpen: isOpenAdd, + onOpen: onOpenAdd, + onClose: onCloseAdd, + } = useDisclosure(); const user = useContext(UserContext); - let color = Color.Red - const doReportsExist = reports.length > 0 + const doReportsExist = reports.length > 0; - // no reports so gray it out - if (doReportsExist === false) { - color = Color.Gray - } + const color = doReportsExist ? Color.Red : Color.Gray; + + // for (const report in reports) { + // const contents = report["Content"].split(","); + // report["Content"] = contents[0]; + // report["Notified"] = contents[1]; + // report["Explanation"] = contents[2]; + // } const DisplayReportsModal = () => { return ( - + View {CommentType.Report} - + - {reports.map( - (report) => ( - - {/* TODO: add UserDisplay card once pr merged in*/} - + {reports.map((report) => ( + + {/* TODO: add UserDisplay card once pr merged in*/} + {/* {TODO: show time} */} - - Reason for report: - - - saveEditedReport(setReports, reports, report, - createNewReport(user, value as ReportOptions, report.Notified, report.Explanation))} - > - - - - - - Supervisor notified reasonably in advance: - - - saveEditedReport(setReports, reports, report, - createNewReport(user, report.Content, value, report.Explanation))} - > - - {isEditable && ( - <> - - - )} - - - - - - Explanation: - - Reason for report: + + saveEditedReport( + setReports, + reports, + report, + createNewReport( + user, + value as ReportOptions, + report.Notified, + report.Explanation + ) + ) + } + > + + + + + + Supervisor notified reasonably in advance: + + + saveEditedReport( + setReports, + reports, + report, + createNewReport( + user, + report.Content, + value, + report.Explanation + ) + ) + } + > + + {isEditable && ( + <> + + + )} + + + + + Explanation: + - saveEditedReport(setReports, reports, report, - createNewReport(user, report.Content, report.Notified, value))} - > - - {isEditable && ( - <> - - - )} + defaultValue={report.Explanation} + onSubmit={(value) => + saveEditedReport( + setReports, + reports, + report, + createNewReport( + user, + report.Content, + report.Notified, + value + ) + ) + } + > + + {isEditable && ( + <> + + + )} - {/* TODO: add editable controls specifically with only enum options*/} - } onClick={() => deleteComment(onCloseDisplay, setReports, reports, CommentType.Report, report)} /> - - - - ))} + {/* TODO: add editable controls specifically with only enum options*/} + } + onClick={() => + deleteComment( + onCloseDisplay, + setReports, + reports, + CommentType.Report, + report + ) + } + /> + + + + ))} - - + - ) - } + ); + }; const AddReportModal = () => { - const [submitDisabled, setSubmitDisabled] = useState(false); + const [submitDisabled, setSubmitDisabled] = useState(false); const [reason, setReason] = useState(ReportOptions.Late); - const [notify, setNotify] = useState('Yes'); - const [explanation, setExplanation] = useState(''); + const [notify, setNotify] = useState("Yes"); + const [explanation, setExplanation] = useState(""); const user = useContext(UserContext); const toast = useToast(); const handleReasonChange = (option) => { setReason(option as ReportOptions); - } + }; const handleSubmit = (e) => { e.preventDefault(); - if (reports.filter(report => report.Content === reason).length === 0) { - setReports([...reports, createNewReport(user, reason, notify, explanation)]); + if (reports.filter((report) => report.Content === reason).length === 0) { + setReports([ + ...reports, + createNewReport(user, reason, notify, explanation), + ]); toast({ - title: 'Report submitted.', + title: "Report submitted.", description: "We've received your report.", - status: 'success', + status: "success", duration: 9000, isClosable: true, }); - } else { + } else { toast({ - title: 'Report submission failed.', + title: "Report submission failed.", description: "There was a problem with your report. Please try again", - status: 'error', + status: "error", duration: 9000, isClosable: true, }); } - onCloseAdd() + onCloseAdd(); }; return ( - + - - {CommentType.Report} - + + + {CommentType.Report} + + - +
- - Select reason for report: + + Select reason for report: - + Tardy Absent Left Early @@ -243,39 +303,48 @@ export default function ShowReportModal({ - Did the associate notify the supervisor reasonably in advance? - - - Yes - No - - + + Did the associate notify the supervisor reasonably in + advance? + + + + Yes + No + + - Why did the associate arrive late/no show/leave early? - - setExplanation(e.target.value)} - /> - } - /> - + + Why did the associate arrive late/no show/leave early? + + + setExplanation(e.target.value)} + /> + } /> + -
- - + @@ -283,39 +352,45 @@ export default function ShowReportModal({
- ) - } + ); + }; return ( <> - {doReportsExist ? + {doReportsExist ? ( <> - {isEditable && + {isEditable && ( } + {isEditable && ( + + )} - } + )} diff --git a/apps/frontend/src/components/TimeCardPage/TimeTableRow.tsx b/apps/frontend/src/components/TimeCardPage/TimeTableRow.tsx index 670879e..c7ebce5 100644 --- a/apps/frontend/src/components/TimeCardPage/TimeTableRow.tsx +++ b/apps/frontend/src/components/TimeCardPage/TimeTableRow.tsx @@ -1,84 +1,99 @@ -import React, { useEffect, useState } from 'react'; -import 'react-time-picker/dist/TimePicker.css'; -import 'react-clock/dist/Clock.css'; -import { Fragment } from 'react'; +import React, { useEffect, useState } from "react"; +import "react-time-picker/dist/TimePicker.css"; +import "react-clock/dist/Clock.css"; +import { Fragment } from "react"; -import { Td } from '@chakra-ui/react'; +import { Td } from "@chakra-ui/react"; -import { TimeEntry } from './CellTypes/TimeEntry'; -import { Duration } from './CellTypes/HoursCell' -import { DateCell } from './CellTypes/DateCell'; -import { TypeCell } from './CellTypes/CellType'; -import { CommentCell } from './CellTypes/CommentCell'; -import { RowSchema } from '../../schemas/RowSchema'; -import ApiClient from 'src/components/Auth/apiClient' +import { TimeEntry } from "./CellTypes/TimeEntry"; +import { Duration } from "./CellTypes/HoursCell"; +import { DateCell } from "./CellTypes/DateCell"; +import { TypeCell } from "./CellTypes/CellType"; +import { CommentCell } from "./CellTypes/CommentCell"; +import { RowSchema } from "../../schemas/RowSchema"; +import ApiClient from "src/components/Auth/apiClient"; -import * as updateSchemas from 'src/schemas/backend/UpdateTimesheet' -import apiClient from 'src/components/Auth/apiClient'; -import { UserSchema } from 'src/schemas/UserSchema'; +import * as updateSchemas from "src/schemas/backend/UpdateTimesheet"; +import apiClient from "src/components/Auth/apiClient"; +import { UserSchema } from "src/schemas/UserSchema"; interface RowProps { - row: RowSchema; - prevDate: number; - onRowChange: Function; - TimesheetID: number; + row: RowSchema; + prevDate: number; + onRowChange: Function; + TimesheetID: number; } - - - function Row(props: RowProps) { - - const [fields, setFields] = useState(undefined); - - const updateField = (key, value) => { - - const newFields = { - ...fields, - [key]: value - } - - setFields(newFields); - props.onRowChange(newFields); - //Send a request to update the db on this item being changed - ApiClient.updateTimesheet(updateSchemas.TimesheetUpdateRequest.parse({ - TimesheetID: props.TimesheetID, - Operation: updateSchemas.TimesheetOperations.UPDATE, - Payload: updateSchemas.UpdateRequest.parse({ - Type: updateSchemas.TimesheetListItems.TABLEDATA, - Id: props.row.UUID, - Attribute: key, - Data: value - }) - })); - - } - - useEffect(() => { - if (props.row !== undefined) { - setFields(RowSchema.parse(props.row)); - } - }, []) - - if (fields !== undefined) { - const items = { - "Type": , - "Date": , - "Clock-in": , - "Clock-out": , - "Hours": , - "Comment": , - } - const itemOrdering = ["Type", "Date", "Clock-in", "Clock-out", "Hours", "Comment"]; - - return - {itemOrdering.map((entry) => {items[entry]})} - - - } else { - return - - + const [fields, setFields] = useState(undefined); + + const updateField = (key, value) => { + const newFields = { + ...fields, + [key]: value, + }; + + setFields(newFields); + props.onRowChange(newFields); + //Send a request to update the db on this item being changed + ApiClient.updateTimesheet( + updateSchemas.TimesheetUpdateRequest.parse({ + TimesheetID: props.TimesheetID, + Operation: updateSchemas.TimesheetOperations.UPDATE, + Payload: updateSchemas.UpdateRequest.parse({ + Type: updateSchemas.TimesheetListItems.TABLEDATA, + Id: props.row.UUID, + Attribute: key, + Data: value, + }), + }) + ); + }; + + useEffect(() => { + if (props.row !== undefined) { + setFields(RowSchema.parse(props.row)); } + }, []); + + if (fields !== undefined) { + const items = { + Type: , + Date: , + "Clock-in": ( + + ), + "Clock-out": ( + + ), + Hours: , + Comment: ( + + ), + }; + const itemOrdering = [ + "Type", + "Date", + "Clock-in", + "Clock-out", + "Hours", + "Comment", + ]; + + return ( + + {itemOrdering.map((entry) => ( + {items[entry]} + ))} + + ); + } else { + return ; + } } export default Row; From f3b0bdb35bad611196e4f43e1280579d624dbb9b Mon Sep 17 00:00:00 2001 From: Kaylee Wu Date: Sun, 31 Mar 2024 15:46:06 -0400 Subject: [PATCH 14/14] fixed save report bug --- README.md | 6 - apps/backend/README.md | 70 +-- apps/backend/src/aws/auth.service.ts | 6 +- apps/backend/src/aws/cognito/Roles.ts | 8 +- .../src/aws/cognito/cognito.wrapper.ts | 41 +- .../middleware/authentication.middleware.ts | 2 +- apps/backend/src/constants.ts | 2 +- apps/backend/src/db/Timesheet.ts | 12 +- .../src/db/frontend/TimesheetSchema.ts | 43 +- apps/backend/src/db/schemas/Timesheet.ts | 79 +-- .../backend/src/db/schemas/UpdateTimesheet.ts | 107 +++-- .../src/db/timesheets/EntryOperations.ts | 1 - apps/backend/src/db/timesheets/Formatter.ts | 163 ++++--- .../src/db/timesheets/FrontendConversions.ts | 190 ++++---- apps/backend/src/dynamodb.ts | 20 +- apps/backend/src/main.ts | 14 +- apps/backend/src/users/user.service.ts | 20 +- apps/backend/src/users/users.controller.ts | 8 +- .../src/utils/decorators/user.decorator.ts | 6 +- apps/backend/test/UploadTimesheet.ts | 105 ++-- apps/frontend/README.md | 81 ++-- .../src/components/Auth/AuthWrapper.tsx | 12 +- .../src/components/HomePage/Announcements.tsx | 57 ++- .../src/components/HomePage/HomePage.tsx | 17 +- .../src/components/HomePage/Messages.tsx | 57 ++- .../components/HomePage/MonthAtAGlance.tsx | 41 +- .../frontend/src/components/NavBar/NavBar.tsx | 78 +-- .../src/components/SignOut/Signout.tsx | 15 +- .../TimeCardPage/AggregationTable.tsx | 141 +++--- .../TimeCardPage/CellTypes/CommentCell.tsx | 22 +- .../CommentModals/ShowCommentModal.tsx | 454 ++++++++++-------- .../CommentModals/ShowReportModal.tsx | 7 - .../TimeCardPage/CellTypes/DateCell.tsx | 26 +- .../TimeCardPage/CellTypes/HoursCell.tsx | 21 +- .../TimeCardPage/CellTypes/TimeEntry.tsx | 94 ++-- .../components/TimeCardPage/CommentModal.tsx | 119 ++--- .../TimeCardPage/SelectWeekCard.tsx | 18 +- .../src/components/TimeCardPage/TimeTable.tsx | 155 +++--- .../components/TimeCardPage/UserContext.tsx | 6 +- .../src/components/TimeCardPage/utils.tsx | 50 +- apps/frontend/src/constants.tsx | 7 +- .../frontend/src/schemas/backend/Timesheet.ts | 81 ++-- .../src/schemas/backend/UpdateTimesheet.ts | 102 ++-- nx.json | 24 +- 44 files changed, 1390 insertions(+), 1198 deletions(-) diff --git a/README.md b/README.md index 16ea193..9fb8d53 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,20 @@ ✨ **This workspace has been generated by [Nx, a Smart, fast and extensible build system.](https://nx.dev)** ✨ - ## Start the app run `npm i` - will install all dependencies for the backend you must add an .env, reach out to tech leads/any one on the team for it. - to run both backend and frontend at the same time run: `npx nx run-many -t start` to start them individually run: `npx nx start backend` or npx nx start frontend` - to run prettier and check all files run: `npx nx format:check --all` to run prettier and format all files run: `npx nx format:write --all` - - - ## Generate code If you happen to use Nx plugins, you can leverage code generators that might come with it. diff --git a/apps/backend/README.md b/apps/backend/README.md index ea41f81..88786bf 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -1,52 +1,56 @@ ![](https://static.wixstatic.com/media/1193ef_371853f9145b445fb883f16ed7741b60~mv2.jpg/v1/crop/x_0,y_2,w_1842,h_332/fill/w_466,h_84,al_c,q_80,usm_0.66_1.00_0.01,enc_auto/Breaktime%20Logo%20Comfortaa-2.jpg) # Breaktime Backend -This repo contains all the code for the backend of Project Breaktime, a project developing a time-tracking dashboard. This project utilizes a ReactJS (Frontend) and NodeJS (Backend). -## Installation +This repo contains all the code for the backend of Project Breaktime, a project developing a time-tracking dashboard. This project utilizes a ReactJS (Frontend) and NodeJS (Backend). + +## Installation + To fully setup the backend follow these steps: -Nagivate to the root of this repo and run the following command +Nagivate to the root of this repo and run the following command -1. Ensure [node](https://nodejs.org/en/download) and [react](https://legacy.reactjs.org/docs/getting-started.html) are installed. +1. Ensure [node](https://nodejs.org/en/download) and [react](https://legacy.reactjs.org/docs/getting-started.html) are installed. -2. Run the following command to install all necessary packages. - ``` - npm install - ``` -3. Next create a file at the root of the project named `.env`. Ask a member of the development team on breaktime for the contents of this file. These are the private tokens required to run certain parts of the backend (Connecting to the Database, etc). +2. Run the following command to install all necessary packages. + ``` + npm install + ``` +3. Next create a file at the root of the project named `.env`. Ask a member of the development team on breaktime for the contents of this file. These are the private tokens required to run certain parts of the backend (Connecting to the Database, etc). 4. Now run `npm start` and ensure that no errors show up, if you do not get any errors in the startup everything is successfully running! +## Reading Material + +To learn more about the various technologies utilized here you can read more about them below: + +- [What is backend development?](https://www.upwork.com/resources/beginners-guide-back-end-development#what-is) +- [NestJS](https://docs.nestjs.com/) : The framework utilized for our backend endpoints +- [AWS Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html) : What is used for our user authentication -## Reading Material -To learn more about the various technologies utilized here you can read more about them below: -* [What is backend development?](https://www.upwork.com/resources/beginners-guide-back-end-development#what-is) -* [NestJS](https://docs.nestjs.com/) : The framework utilized for our backend endpoints -* [AWS Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html) : What is used for our user authentication +## Code Overview: -## Code Overview: -Our file structure is outlined below, while not all files are documented here these are several important ones for our design: +Our file structure is outlined below, while not all files are documented here these are several important ones for our design: ``` breaktime-backend/ -├─ src/ - Directory housing almost all code +├─ src/ - Directory housing almost all code │ ├─ aws/ -| | ├─ cognito/ AWS Coginto modules for various functionality -| | ├─ decorators/ Decorators for role-based functionality -| | ├─ middleware/ Functionality for actually authenticating a user -| | ├─ auth.controller.ts - Endpoints requiring authentication through AWS Cognito -| | ├─ auth.module.ts - Nest configuration file -| ├─ db/ Modules containing database functionality -| ├─ users/ Modules for users -| ├─ utils/ Utility modules -| ├─ app.module.ts -| ├─ constants.ts - Global constants -| ├─ dynamodb.ts - Main interface for interacting with database -| ├─ main.ts - main method / root of the project -├─ test/ - Directory containing test files -| ├─ UploadTimesheet.ts - A file used in debugging to upload arbitrary timesheets -├─ .gitignore - files ignored by git -├─ package.json - project configuration file +| | ├─ cognito/ AWS Coginto modules for various functionality +| | ├─ decorators/ Decorators for role-based functionality +| | ├─ middleware/ Functionality for actually authenticating a user +| | ├─ auth.controller.ts - Endpoints requiring authentication through AWS Cognito +| | ├─ auth.module.ts - Nest configuration file +| ├─ db/ Modules containing database functionality +| ├─ users/ Modules for users +| ├─ utils/ Utility modules +| ├─ app.module.ts +| ├─ constants.ts - Global constants +| ├─ dynamodb.ts - Main interface for interacting with database +| ├─ main.ts - main method / root of the project +├─ test/ - Directory containing test files +| ├─ UploadTimesheet.ts - A file used in debugging to upload arbitrary timesheets +├─ .gitignore - files ignored by git +├─ package.json - project configuration file ├─ README.md ``` diff --git a/apps/backend/src/aws/auth.service.ts b/apps/backend/src/aws/auth.service.ts index ceecba6..f3d8c73 100644 --- a/apps/backend/src/aws/auth.service.ts +++ b/apps/backend/src/aws/auth.service.ts @@ -21,7 +21,11 @@ export class AuthService { console.log( "Testing environment - mock user will be used, authentication skipped" ); - return { isValidated: true, groups: mockSupervisor["cognito:groups"], sub: mockSupervisor.sub }; + return { + isValidated: true, + groups: mockSupervisor["cognito:groups"], + sub: mockSupervisor.sub, + }; } try { const userPayload = await this.cognitoService.validate(jwt); diff --git a/apps/backend/src/aws/cognito/Roles.ts b/apps/backend/src/aws/cognito/Roles.ts index ad11346..b088d05 100644 --- a/apps/backend/src/aws/cognito/Roles.ts +++ b/apps/backend/src/aws/cognito/Roles.ts @@ -1,5 +1,5 @@ export enum CognitoRoles { - ADMIN = 'breaktime-admin', - SUPERVISOR = 'breaktime-supervisor', - ASSOCIATE = 'breaktime-associate' -} \ No newline at end of file + ADMIN = "breaktime-admin", + SUPERVISOR = "breaktime-supervisor", + ASSOCIATE = "breaktime-associate", +} diff --git a/apps/backend/src/aws/cognito/cognito.wrapper.ts b/apps/backend/src/aws/cognito/cognito.wrapper.ts index 3236c1e..997527a 100644 --- a/apps/backend/src/aws/cognito/cognito.wrapper.ts +++ b/apps/backend/src/aws/cognito/cognito.wrapper.ts @@ -47,25 +47,27 @@ export class CognitoWrapper { // Loop through all roles (aka Cognito groups) to get the user data in that group // A user can only ever be in one group, so there should be no duplicate users - var userData = [] + var userData = []; - const groups = Object.values(CognitoRoles) + const groups = Object.values(CognitoRoles); for (let role of groups) { - console.log(role) - console.log("Getting users in group %s", role) - const usersInGroup = await this.listUsersInGroup(this.userPoolId, role) + console.log(role); + console.log("Getting users in group %s", role); + const usersInGroup = await this.listUsersInGroup(this.userPoolId, role); if (userData == null) { - throw new Error("Issue with retrieving user data from user pool for group."); + throw new Error( + "Issue with retrieving user data from user pool for group." + ); } // Parse each user into a CognitoUser object, and set their group attribute - const modifiedUsers = usersInGroup.map((user) => { + const modifiedUsers = usersInGroup.map((user) => { const parsedUser = CognitoUser.parse(user); - parsedUser.Attributes.push({Name: "cognito:groups", Value: role}); + parsedUser.Attributes.push({ Name: "cognito:groups", Value: role }); return parsedUser; - }) - userData = [...userData, ...modifiedUsers] + }); + userData = [...userData, ...modifiedUsers]; } return userData; @@ -83,12 +85,11 @@ export class CognitoWrapper { // Get all users first const userData = await this.getUsers(); - return userData - .filter((user) => - userIDs.includes( - user.Attributes.find((attribute) => (attribute.Name = "sub")).Value - ) - ); + return userData.filter((user) => + userIDs.includes( + user.Attributes.find((attribute) => (attribute.Name = "sub")).Value + ) + ); } catch (error) { console.log(error); return []; @@ -120,13 +121,17 @@ export class CognitoWrapper { * Gets a list of raw user data from a specified Cognito user pool for the given group, with maximum of 'limit' users. * If an error occurs, log the error and return an empty list. */ - async listUsersInGroup(userPoolId: string, groupName: string, limit?: number) { + async listUsersInGroup( + userPoolId: string, + groupName: string, + limit?: number + ) { try { const command = new ListUsersInGroupCommand({ UserPoolId: userPoolId, GroupName: groupName, Limit: limit, - }) + }); return this.serviceProvider.send(command).then((data) => data.Users); } catch (error) { diff --git a/apps/backend/src/aws/middleware/authentication.middleware.ts b/apps/backend/src/aws/middleware/authentication.middleware.ts index db3192f..fc4adf8 100644 --- a/apps/backend/src/aws/middleware/authentication.middleware.ts +++ b/apps/backend/src/aws/middleware/authentication.middleware.ts @@ -16,7 +16,7 @@ export class AuthenticationMiddleware implements NestMiddleware { req.user = { isValidated: true, groups: mockSupervisor["cognito:groups"], - sub: mockSupervisor.sub + sub: mockSupervisor.sub, }; return next(); } diff --git a/apps/backend/src/constants.ts b/apps/backend/src/constants.ts index cd92107..b6d16a7 100644 --- a/apps/backend/src/constants.ts +++ b/apps/backend/src/constants.ts @@ -1 +1 @@ -export const TIMESHEET_DURATION = 7 \ No newline at end of file +export const TIMESHEET_DURATION = 7; diff --git a/apps/backend/src/db/Timesheet.ts b/apps/backend/src/db/Timesheet.ts index 0f9533e..4d00b4c 100644 --- a/apps/backend/src/db/Timesheet.ts +++ b/apps/backend/src/db/Timesheet.ts @@ -39,11 +39,7 @@ export const TimeEntrySchema = z.object({ * SubmittedDate reflects the time of last submission, whether from associate, supervisor, or admin. */ export const StatusSchema = z.object({ - StatusType: z.enum([ - "HoursSubmitted", - "HoursReviewed", - "Finalized", - ]), + StatusType: z.enum(["HoursSubmitted", "HoursReviewed", "Finalized"]), SubmittedDateTime: z.number(), }); @@ -74,7 +70,7 @@ export const TimeSheetSchema = z.object({ export type TimeSheetSchema = z.infer; export enum TimesheetStatus { - HOURS_SUBMITTED="HoursSubmitted", - HOURS_REVIEWED="HoursReviewed", - FINALIZED="Finalized" + HOURS_SUBMITTED = "HoursSubmitted", + HOURS_REVIEWED = "HoursReviewed", + FINALIZED = "Finalized", } diff --git a/apps/backend/src/db/frontend/TimesheetSchema.ts b/apps/backend/src/db/frontend/TimesheetSchema.ts index 9e9b127..deac3c8 100644 --- a/apps/backend/src/db/frontend/TimesheetSchema.ts +++ b/apps/backend/src/db/frontend/TimesheetSchema.ts @@ -3,34 +3,35 @@ ////////////////////////////////////////////////////////////////////////// import { z } from "zod"; -import {RowSchema, ScheduledRowSchema, CommentSchema} from './RowSchema'; +import { RowSchema, ScheduledRowSchema, CommentSchema } from "./RowSchema"; -// The status is either undefined, for not being at that stage yet, or -// contains the date and author of approving this submission -export const StatusEntryType = z.union( - [z.object({ - Date: z.number(), - AuthorID: z.string() - }), - z.undefined()]); +// The status is either undefined, for not being at that stage yet, or +// contains the date and author of approving this submission +export const StatusEntryType = z.union([ + z.object({ + Date: z.number(), + AuthorID: z.string(), + }), + z.undefined(), +]); -// Status type contains the three stages of the pipeline we have defined +// Status type contains the three stages of the pipeline we have defined export const StatusType = z.object({ - HoursSubmitted: StatusEntryType, + HoursSubmitted: StatusEntryType, HoursReviewed: StatusEntryType, - Finalized: StatusEntryType + Finalized: StatusEntryType, }); -export type StatusType = z.infer +export type StatusType = z.infer; export const FrontendTimeSheetSchema = z.object({ - TimesheetID: z.number(), - UserID: z.string(), + TimesheetID: z.number(), + UserID: z.string(), StartDate: z.number(), - Status: StatusType, - CompanyID: z.string(), - TableData: z.array(RowSchema), + Status: StatusType, + CompanyID: z.string(), + TableData: z.array(RowSchema), ScheduleTableData: z.union([z.undefined(), z.array(ScheduledRowSchema)]), - WeekNotes: z.union([z.undefined(), z.array(CommentSchema)]), -}); + WeekNotes: z.union([z.undefined(), z.array(CommentSchema)]), +}); -export type FrontendTimeSheetSchema = z.infer +export type FrontendTimeSheetSchema = z.infer; diff --git a/apps/backend/src/db/schemas/Timesheet.ts b/apps/backend/src/db/schemas/Timesheet.ts index 7965309..eb16116 100644 --- a/apps/backend/src/db/schemas/Timesheet.ts +++ b/apps/backend/src/db/schemas/Timesheet.ts @@ -6,23 +6,23 @@ import { z } from "zod"; */ export const NoteSchema = z.object({ Type: z.enum(["Comment", "Report"]), - EntryID: z.string(), + EntryID: z.string(), AuthorUUID: z.string(), DateTime: z.number(), Content: z.string(), State: z.enum(["Active", "Deleted"]), -}) +}); /** * Represents the database schema for a schedule shift entry, made by a supervisor or admin */ export const ScheduleEntrySchema = z.object({ - EntryID: z.string(), - Date: z.number(), + EntryID: z.string(), + Date: z.number(), StartDateTime: z.number().optional(), EndDateTime: z.number().optional(), - AuthorUUID: z.string() -}) + AuthorUUID: z.string(), +}); /** * Represents the database schema for a clockin/clockout pair in epoch @@ -31,8 +31,7 @@ export const TimeEntrySchema = z.object({ StartDateTime: z.number().optional(), EndDateTime: z.number().optional(), AuthorUUID: z.string(), -}) - +}); /* Supported type of cells for each row in a timesheet @@ -42,58 +41,60 @@ export const TimeEntrySchema = z.object({ export enum CellType { REGULAR_LEGACY = "Regular", // No longer using this format for data, but some older timesheet entries may have the 'legacy' type REGULAR = "Time Worked", - PTO = "PTO" + PTO = "PTO", } /** - * Represents the database schema for a single shift or entry in the weekly timesheet. + * Represents the database schema for a single shift or entry in the weekly timesheet. */ export const TimesheetEntrySchema = z.object({ - Type: z.enum([CellType.REGULAR, CellType.REGULAR_LEGACY, CellType.PTO]).transform((cellType) => cellType === CellType.REGULAR_LEGACY ? CellType.REGULAR : cellType), - EntryID: z.string(), - Date: z.number(), + Type: z + .enum([CellType.REGULAR, CellType.REGULAR_LEGACY, CellType.PTO]) + .transform((cellType) => + cellType === CellType.REGULAR_LEGACY ? CellType.REGULAR : cellType + ), + EntryID: z.string(), + Date: z.number(), AssociateTimes: TimeEntrySchema.optional(), SupervisorTimes: TimeEntrySchema.optional(), AdminTimes: TimeEntrySchema.optional(), Note: z.array(NoteSchema).optional(), -}) - +}); -// The status is either undefined, for not being at that stage yet, or -// contains the date and author of approving this submission -export const StatusEntryType = z.union( - [z.object({ - Date: z.number(), - AuthorID: z.string() - }), - z.undefined()]); +// The status is either undefined, for not being at that stage yet, or +// contains the date and author of approving this submission +export const StatusEntryType = z.union([ + z.object({ + Date: z.number(), + AuthorID: z.string(), + }), + z.undefined(), +]); -// Status type contains the four stages of the pipeline we have defined +// Status type contains the four stages of the pipeline we have defined export const TimesheetStatus = z.object({ - HoursSubmitted: StatusEntryType, + HoursSubmitted: StatusEntryType, HoursReviewed: StatusEntryType, - Finalized: StatusEntryType + Finalized: StatusEntryType, }); - - /** * Represents the database schema for a weekly timesheet */ export const TimeSheetSchema = z.object({ - TimesheetID: z.number(), - UserID: z.string(), + TimesheetID: z.number(), + UserID: z.string(), StartDate: z.number(), Status: TimesheetStatus, - CompanyID: z.string(), - HoursData: z.array(TimesheetEntrySchema).default([]), + CompanyID: z.string(), + HoursData: z.array(TimesheetEntrySchema).default([]), ScheduleData: z.array(ScheduleEntrySchema).default([]), WeekNotes: z.array(NoteSchema).default([]), -}) +}); -export type TimesheetStatus = z.infer -export type TimeEntrySchema = z.infer -export type ScheduleEntrySchema = z.infer -export type NoteSchema = z.infer -export type TimesheetEntrySchema = z.infer -export type TimeSheetSchema = z.infer +export type TimesheetStatus = z.infer; +export type TimeEntrySchema = z.infer; +export type ScheduleEntrySchema = z.infer; +export type NoteSchema = z.infer; +export type TimesheetEntrySchema = z.infer; +export type TimeSheetSchema = z.infer; diff --git a/apps/backend/src/db/schemas/UpdateTimesheet.ts b/apps/backend/src/db/schemas/UpdateTimesheet.ts index 0275c7f..bb0efff 100644 --- a/apps/backend/src/db/schemas/UpdateTimesheet.ts +++ b/apps/backend/src/db/schemas/UpdateTimesheet.ts @@ -1,6 +1,10 @@ import { z } from "zod"; -import { RowSchema, CommentSchema, ScheduledRowSchema } from "../frontend/RowSchema"; -import * as dbTypes from '../schemas/Timesheet' +import { + RowSchema, + CommentSchema, + ScheduledRowSchema, +} from "../frontend/RowSchema"; +import * as dbTypes from "../schemas/Timesheet"; import { TimesheetStatus } from "../Timesheet"; /* @@ -16,13 +20,12 @@ import { TimesheetStatus } from "../Timesheet"; CREATE-TIMESHEET - Operation for creating a timesheet, if it would be useful to have in the future. */ export const enum TimesheetOperations { - INSERT = "INSERT", - UPDATE = "UPDATE", - DELETE = "DELETE", - STATUS_CHANGE = "STATUS_CHANGE", - CREATE_TIMESHEET = "CREATE_TIMESHEET" -} - + INSERT = "INSERT", + UPDATE = "UPDATE", + DELETE = "DELETE", + STATUS_CHANGE = "STATUS_CHANGE", + CREATE_TIMESHEET = "CREATE_TIMESHEET", +} /* The available types of items that are currently supported in the timesheet that list operations can be performed on. @@ -31,12 +34,16 @@ export const enum TimesheetOperations { WEEKNOTES - the comments left by an employer for that week */ export const enum TimesheetListItems { - TABLEDATA = "TABLEDATA", - SCHEDULEDATA = "SCHEDULEDATA", // TODO : delete this - WEEKNOTES = "WEEKNOTES" + TABLEDATA = "TABLEDATA", + SCHEDULEDATA = "SCHEDULEDATA", // TODO : delete this + WEEKNOTES = "WEEKNOTES", } -const availableListTypes = z.enum([TimesheetListItems.TABLEDATA, TimesheetListItems.SCHEDULEDATA, TimesheetListItems.WEEKNOTES]) +const availableListTypes = z.enum([ + TimesheetListItems.TABLEDATA, + TimesheetListItems.SCHEDULEDATA, + TimesheetListItems.WEEKNOTES, +]); /* The schema for a delete request @@ -44,10 +51,10 @@ const availableListTypes = z.enum([TimesheetListItems.TABLEDATA, TimesheetListIt @Id: The id of the item we are deleting - to know what to remove */ export const DeleteRequest = z.object({ - Type: availableListTypes, - Id: z.string() -}) -export type DeleteRequest = z.infer + Type: availableListTypes, + Id: z.string(), +}); +export type DeleteRequest = z.infer; /* The schema for an insert request for an item @@ -55,10 +62,15 @@ export type DeleteRequest = z.infer @Item: The item we are actually inserting, should be the actual item itself. */ export const InsertRequest = z.object({ - Type: availableListTypes, - Item: z.union([RowSchema, CommentSchema, ScheduledRowSchema, dbTypes.TimesheetEntrySchema]), -}) -export type InsertRequest = z.infer + Type: availableListTypes, + Item: z.union([ + RowSchema, + CommentSchema, + ScheduledRowSchema, + dbTypes.TimesheetEntrySchema, + ]), +}); +export type InsertRequest = z.infer; /* Schema for updating an item from the three possible list of items in the timesheet @Type: The field of the timesheet we are updating from the three supported @@ -67,12 +79,12 @@ export type InsertRequest = z.infer @Data: The payload we are updating this attribute to be - can be a wide range of things currently */ export const UpdateRequest = z.object({ - Type: availableListTypes, - Id: z.string(), - Attribute: z.string(), - Data: z.any() -}) -export type UpdateRequest = z.infer + Type: availableListTypes, + Id: z.string(), + Attribute: z.string(), + Data: z.any(), +}); +export type UpdateRequest = z.infer; /* Schema for changing the status of a timesheet @@ -80,13 +92,17 @@ export type UpdateRequest = z.infer @AssociateId: The id of the associate whose timesheet is being submitted */ export const StatusChangeRequest = z.object({ - TimesheetId: z.number(), - AssociateId: z.string(), - authorId: z.string(), - dateSubmitted: z.number(), - statusType: z.enum([TimesheetStatus.FINALIZED, TimesheetStatus.HOURS_REVIEWED, TimesheetStatus.HOURS_SUBMITTED]) -}) -export type StatusChangeRequest = z.infer + TimesheetId: z.number(), + AssociateId: z.string(), + authorId: z.string(), + dateSubmitted: z.number(), + statusType: z.enum([ + TimesheetStatus.FINALIZED, + TimesheetStatus.HOURS_REVIEWED, + TimesheetStatus.HOURS_SUBMITTED, + ]), +}); +export type StatusChangeRequest = z.infer; /* The main request body that is used to determine what we should be updating in a request @TimesheetID: The id of the timesheet we are updating @@ -94,15 +110,14 @@ export type StatusChangeRequest = z.infer @Payload: The contents to be used in the operation for updating this. */ export const TimesheetUpdateRequest = z.object({ - TimesheetID: z.number(), - Operation: z.enum([ - TimesheetOperations.INSERT, - TimesheetOperations.UPDATE, - TimesheetOperations.DELETE, - TimesheetOperations.STATUS_CHANGE, - TimesheetOperations.CREATE_TIMESHEET - ]), - Payload: z.any() -}) -export type TimesheetUpdateRequest = z.infer - + TimesheetID: z.number(), + Operation: z.enum([ + TimesheetOperations.INSERT, + TimesheetOperations.UPDATE, + TimesheetOperations.DELETE, + TimesheetOperations.STATUS_CHANGE, + TimesheetOperations.CREATE_TIMESHEET, + ]), + Payload: z.any(), +}); +export type TimesheetUpdateRequest = z.infer; diff --git a/apps/backend/src/db/timesheets/EntryOperations.ts b/apps/backend/src/db/timesheets/EntryOperations.ts index 0cced00..1d98ad0 100644 --- a/apps/backend/src/db/timesheets/EntryOperations.ts +++ b/apps/backend/src/db/timesheets/EntryOperations.ts @@ -86,7 +86,6 @@ export class frontendEntryConversions { switch (body.Type) { case requestSchemas.TimesheetListItems.TABLEDATA: const convertedKey = this.hoursDataMappings[body.Attribute].finalKey; - console.log(convertedKey); const convertedValue = this.hoursDataMappings[ body.Attribute ].conversionFn(body.Data); diff --git a/apps/backend/src/db/timesheets/Formatter.ts b/apps/backend/src/db/timesheets/Formatter.ts index 764a072..ee8ef98 100644 --- a/apps/backend/src/db/timesheets/Formatter.ts +++ b/apps/backend/src/db/timesheets/Formatter.ts @@ -1,96 +1,103 @@ -import * as timesheetSchemas from 'src/db/schemas/Timesheet' +import * as timesheetSchemas from "src/db/schemas/Timesheet"; -import * as constants from 'src/constants' -import { v4 as uuidv4 } from 'uuid'; +import * as constants from "src/constants"; +import { v4 as uuidv4 } from "uuid"; -import {UserTimesheets, WriteEntryToTable} from 'src/dynamodb' -import { DBToFrontend } from './FrontendConversions'; - -const moment = require('moment-timezone'); +import { UserTimesheets, WriteEntryToTable } from "src/dynamodb"; +import { DBToFrontend } from "./FrontendConversions"; +const moment = require("moment-timezone"); export class Formatter { - /* + /* Processes the timesheets we are grabbing for a user to ensure they are properly prepared for the user - i.e. any missing days are added, etc. */ - // Fetches timesheets and properly formats them to our frontend data versions. - public static async fetchUserTimesheets(userid: string) { - //Grab timesheets from DB - var timesheets = await UserTimesheets(userid); - //Convert to Frontend equivalents and convert - timesheets = this.format(timesheets); - + // Fetches timesheets and properly formats them to our frontend data versions. + public static async fetchUserTimesheets(userid: string) { + //Grab timesheets from DB + var timesheets = await UserTimesheets(userid); + //Convert to Frontend equivalents and convert + timesheets = this.format(timesheets); - return DBToFrontend.convertTimesheets(timesheets); - } + return DBToFrontend.convertTimesheets(timesheets); + } - // Formats a list of backend / database timesheets to the frontend equivalents. - public static format(timesheets: timesheetSchemas.TimeSheetSchema[]) : timesheetSchemas.TimeSheetSchema[] { - const updatedTimesheets = timesheets.map((timesheet) => { - const [updatedTimesheet, modified] = this.validate(timesheet); - if (modified) { - //If this timesheet was modified we should re-upload it - WriteEntryToTable(updatedTimesheet); - } - return updatedTimesheet; - }) - return updatedTimesheets - - } + // Formats a list of backend / database timesheets to the frontend equivalents. + public static format( + timesheets: timesheetSchemas.TimeSheetSchema[] + ): timesheetSchemas.TimeSheetSchema[] { + const updatedTimesheets = timesheets.map((timesheet) => { + const [updatedTimesheet, modified] = this.validate(timesheet); + if (modified) { + //If this timesheet was modified we should re-upload it + WriteEntryToTable(updatedTimesheet); + } + return updatedTimesheet; + }); + return updatedTimesheets; + } - // Main method all other future methods delegate to / would return to when we are processing a timesheet to convert to frontend - private static validate(timesheet): [timesheetSchemas.TimeSheetSchema, boolean] { - //When more functions are introduced here, create logic to determine whether any modified it to return - return this.ensureAllDays(timesheet); - } + // Main method all other future methods delegate to / would return to when we are processing a timesheet to convert to frontend + private static validate( + timesheet + ): [timesheetSchemas.TimeSheetSchema, boolean] { + //When more functions are introduced here, create logic to determine whether any modified it to return + return this.ensureAllDays(timesheet); + } - private static ensureAllDays(timesheet:timesheetSchemas.TimeSheetSchema): [timesheetSchemas.TimeSheetSchema, boolean] { - /* + private static ensureAllDays( + timesheet: timesheetSchemas.TimeSheetSchema + ): [timesheetSchemas.TimeSheetSchema, boolean] { + /* Ensures that for each day from START_DATE to START_DATE + TIMESHEET_DURATION that each day has at-least one entry */ - var modifiedRows = false; + var modifiedRows = false; - const updatedRows = [] - var endDate = moment.unix(timesheet.StartDate).add(constants.TIMESHEET_DURATION - 1, 'days'); - var currentDate = moment.unix(timesheet.StartDate); + const updatedRows = []; + var endDate = moment + .unix(timesheet.StartDate) + .add(constants.TIMESHEET_DURATION - 1, "days"); + var currentDate = moment.unix(timesheet.StartDate); - timesheet.HoursData?.map((row) => { - const rowDate = moment.unix(row.Date); - while (rowDate.isAfter(currentDate, 'day')) { - modifiedRows = true; - updatedRows.push(this.createEmptyRow(currentDate.unix())); - currentDate = currentDate.add(1, 'day'); - } - updatedRows.push(row); - currentDate = currentDate.add(1, 'day'); - }); - // Fill in remaining daysd - while (!currentDate.isAfter(endDate, 'day')){ - modifiedRows = true; - updatedRows.push(this.createEmptyRow(currentDate.unix())); - currentDate = currentDate.add(1, 'day'); - } - //Returns the updated timesheet and whether or not it was modified - return [ - timesheetSchemas.TimeSheetSchema.parse( - {...timesheet, - HoursData: updatedRows - } - ), modifiedRows]; - - } - //Creates an empty row in the timesheet for a specified date. - private static createEmptyRow(date: number): timesheetSchemas.TimesheetEntrySchema { - return timesheetSchemas.TimesheetEntrySchema.parse({ - Type: timesheetSchemas.CellType.REGULAR, - EntryID: uuidv4(), - Date: date, - AssociateTimes: undefined, - SupervisorTimes: undefined, - AdminTimes: undefined, - Note: [] - }) + timesheet.HoursData?.map((row) => { + const rowDate = moment.unix(row.Date); + while (rowDate.isAfter(currentDate, "day")) { + modifiedRows = true; + updatedRows.push(this.createEmptyRow(currentDate.unix())); + currentDate = currentDate.add(1, "day"); + } + updatedRows.push(row); + currentDate = currentDate.add(1, "day"); + }); + // Fill in remaining daysd + while (!currentDate.isAfter(endDate, "day")) { + modifiedRows = true; + updatedRows.push(this.createEmptyRow(currentDate.unix())); + currentDate = currentDate.add(1, "day"); } -} \ No newline at end of file + //Returns the updated timesheet and whether or not it was modified + return [ + timesheetSchemas.TimeSheetSchema.parse({ + ...timesheet, + HoursData: updatedRows, + }), + modifiedRows, + ]; + } + //Creates an empty row in the timesheet for a specified date. + private static createEmptyRow( + date: number + ): timesheetSchemas.TimesheetEntrySchema { + return timesheetSchemas.TimesheetEntrySchema.parse({ + Type: timesheetSchemas.CellType.REGULAR, + EntryID: uuidv4(), + Date: date, + AssociateTimes: undefined, + SupervisorTimes: undefined, + AdminTimes: undefined, + Note: [], + }); + } +} diff --git a/apps/backend/src/db/timesheets/FrontendConversions.ts b/apps/backend/src/db/timesheets/FrontendConversions.ts index db9de77..4caac77 100644 --- a/apps/backend/src/db/timesheets/FrontendConversions.ts +++ b/apps/backend/src/db/timesheets/FrontendConversions.ts @@ -1,101 +1,115 @@ -import * as dbTypes from '../schemas/Timesheet' -import * as frontendRowTypes from '../frontend/RowSchema' -import * as frontendTimesheetTypes from '../frontend/TimesheetSchema' +import * as dbTypes from "../schemas/Timesheet"; +import * as frontendRowTypes from "../frontend/RowSchema"; +import * as frontendTimesheetTypes from "../frontend/TimesheetSchema"; export class DBToFrontend { - /* + /* Mapper from converting from a Database backend timesheet to a frontend one - */ + */ - // Converts a list of backend timesheets to frontend ones - public static convertTimesheets(timesheets: dbTypes.TimeSheetSchema[]) : frontendTimesheetTypes.FrontendTimeSheetSchema[] { - return timesheets.map((timesheet) => this.toFrontendTimesheet(timesheet)); - } + // Converts a list of backend timesheets to frontend ones + public static convertTimesheets( + timesheets: dbTypes.TimeSheetSchema[] + ): frontendTimesheetTypes.FrontendTimeSheetSchema[] { + return timesheets.map((timesheet) => this.toFrontendTimesheet(timesheet)); + } - // Converts a singular backend timesheet to a frontend one - public static toFrontendTimesheet(timesheet: dbTypes.TimeSheetSchema): frontendTimesheetTypes.FrontendTimeSheetSchema { - return frontendTimesheetTypes.FrontendTimeSheetSchema.parse({ - TimesheetID: timesheet.TimesheetID, - UserID: timesheet.UserID, - StartDate: timesheet.StartDate, - Status: this.toFrontendStatus(timesheet.Status), - CompanyID: timesheet.CompanyID, - TableData: this.toFrontendRows(timesheet.HoursData), - ScheduleTableData: this.toFrontendScheduleData(timesheet.ScheduleData), - WeekNotes: this.toFrontendComments(timesheet.WeekNotes) - }); - } + // Converts a singular backend timesheet to a frontend one + public static toFrontendTimesheet( + timesheet: dbTypes.TimeSheetSchema + ): frontendTimesheetTypes.FrontendTimeSheetSchema { + return frontendTimesheetTypes.FrontendTimeSheetSchema.parse({ + TimesheetID: timesheet.TimesheetID, + UserID: timesheet.UserID, + StartDate: timesheet.StartDate, + Status: this.toFrontendStatus(timesheet.Status), + CompanyID: timesheet.CompanyID, + TableData: this.toFrontendRows(timesheet.HoursData), + ScheduleTableData: this.toFrontendScheduleData(timesheet.ScheduleData), + WeekNotes: this.toFrontendComments(timesheet.WeekNotes), + }); + } - // Converts a backend status to a frontend one - private static toFrontendStatus(status: dbTypes.TimesheetStatus): frontendTimesheetTypes.StatusType { - return frontendTimesheetTypes.StatusType.parse({ - HoursSubmitted: status.HoursSubmitted, - HoursReviewed: status.HoursReviewed, - Finalized: status.Finalized - }) - } + // Converts a backend status to a frontend one + private static toFrontendStatus( + status: dbTypes.TimesheetStatus + ): frontendTimesheetTypes.StatusType { + return frontendTimesheetTypes.StatusType.parse({ + HoursSubmitted: status.HoursSubmitted, + HoursReviewed: status.HoursReviewed, + Finalized: status.Finalized, + }); + } - //Converts a backend row to a frontend one - private static toFrontendRows(rows: dbTypes.TimesheetEntrySchema[]): frontendRowTypes.RowSchema[] { - if (rows === undefined) { - return []; - } - return rows.map((row) => { - return frontendRowTypes.RowSchema.parse({ - UUID: row.EntryID, - Type: row.Type, - Date: row.Date, - Associate: this.toFrontendRowEntry(row.AssociateTimes), - Supervisor: this.toFrontendRowEntry(row.SupervisorTimes), - Admin: this.toFrontendRowEntry(row.AdminTimes), - Comment: this.toFrontendComments(row.Note) - }); - }) + //Converts a backend row to a frontend one + private static toFrontendRows( + rows: dbTypes.TimesheetEntrySchema[] + ): frontendRowTypes.RowSchema[] { + if (rows === undefined) { + return []; } + return rows.map((row) => { + return frontendRowTypes.RowSchema.parse({ + UUID: row.EntryID, + Type: row.Type, + Date: row.Date, + Associate: this.toFrontendRowEntry(row.AssociateTimes), + Supervisor: this.toFrontendRowEntry(row.SupervisorTimes), + Admin: this.toFrontendRowEntry(row.AdminTimes), + Comment: this.toFrontendComments(row.Note), + }); + }); + } - //Converts the backend schedule data to the frontend equivalent. - private static toFrontendScheduleData(rows: dbTypes.ScheduleEntrySchema[]): frontendRowTypes.ScheduledRowSchema[] { - if (rows === undefined) { - return []; - } - return rows.map((row) => { - return frontendRowTypes.ScheduledRowSchema.parse({ - UUID: row.EntryID, - Date: row.Date, - Entry: frontendRowTypes.TimeRowEntry.parse({ - Start: row.StartDateTime, - End: row.EndDateTime, - AuthorID: row.AuthorUUID - }) - }); - }) + //Converts the backend schedule data to the frontend equivalent. + private static toFrontendScheduleData( + rows: dbTypes.ScheduleEntrySchema[] + ): frontendRowTypes.ScheduledRowSchema[] { + if (rows === undefined) { + return []; } + return rows.map((row) => { + return frontendRowTypes.ScheduledRowSchema.parse({ + UUID: row.EntryID, + Date: row.Date, + Entry: frontendRowTypes.TimeRowEntry.parse({ + Start: row.StartDateTime, + End: row.EndDateTime, + AuthorID: row.AuthorUUID, + }), + }); + }); + } - //Converts a backend row entry to a frontend one - private static toFrontendRowEntry(row: dbTypes.TimeEntrySchema): frontendRowTypes.TimeRowEntry { - if (row === undefined) { - return undefined; - } - return frontendRowTypes.TimeRowEntry.parse({ - Start: row.StartDateTime, - End: row.EndDateTime, - AuthorID: row.AuthorUUID - }) + //Converts a backend row entry to a frontend one + private static toFrontendRowEntry( + row: dbTypes.TimeEntrySchema + ): frontendRowTypes.TimeRowEntry { + if (row === undefined) { + return undefined; } + return frontendRowTypes.TimeRowEntry.parse({ + Start: row.StartDateTime, + End: row.EndDateTime, + AuthorID: row.AuthorUUID, + }); + } - //Converts a list of backend comments to frontend equivalents. - private static toFrontendComments(comments: dbTypes.NoteSchema[]): frontendRowTypes.CommentSchema[] { - if (comments === undefined) { - return []; - } - return comments.map((comment) => { - return frontendRowTypes.CommentSchema.parse({ - UUID: comment.EntryID, - AuthorID: comment.AuthorUUID, - Type: comment.Type, - Timestamp: comment.DateTime, - Content: comment.Content, - State: comment.State - }); - }) + //Converts a list of backend comments to frontend equivalents. + private static toFrontendComments( + comments: dbTypes.NoteSchema[] + ): frontendRowTypes.CommentSchema[] { + if (comments === undefined) { + return []; } -} \ No newline at end of file + return comments.map((comment) => { + return frontendRowTypes.CommentSchema.parse({ + UUID: comment.EntryID, + AuthorID: comment.AuthorUUID, + Type: comment.Type, + Timestamp: comment.DateTime, + Content: comment.Content, + State: comment.State, + }); + }); + } +} diff --git a/apps/backend/src/dynamodb.ts b/apps/backend/src/dynamodb.ts index 6a2954b..4ec613d 100644 --- a/apps/backend/src/dynamodb.ts +++ b/apps/backend/src/dynamodb.ts @@ -8,8 +8,8 @@ import { import { unmarshall, marshall } from "@aws-sdk/util-dynamodb"; import * as dotenv from "dotenv"; -import {TimeSheetSchema} from './db/schemas/Timesheet' -import { CompanySchema, UserCompaniesSchema } from './db/schemas/CompanyUsers'; +import { TimeSheetSchema } from "./db/schemas/Timesheet"; +import { CompanySchema, UserCompaniesSchema } from "./db/schemas/CompanyUsers"; dotenv.config(); @@ -128,19 +128,19 @@ export async function GetCompanyData( return companyData[0]; } -export async function WriteEntryToTable(table:TimeSheetSchema): Promise { +export async function WriteEntryToTable( + table: TimeSheetSchema +): Promise { const options = { - removeUndefinedValues: true + removeUndefinedValues: true, }; const params = { - TableName: 'BreaktimeTimesheets', - Item: marshall(table, options), - removeUndefinedValues: true, - }; + TableName: "BreaktimeTimesheets", + Item: marshall(table, options), + removeUndefinedValues: true, + }; - - try { //Input validation - if this fails we do not upload following this as it did not have appropriate types TimeSheetSchema.parse(table); diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 400e0f7..84e54e2 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,15 +1,15 @@ -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; -import {run} from "../test/UploadTimesheet" +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; +import { run } from "../test/UploadTimesheet"; async function bootstrap() { - // UPLOAD mode to allow us to run arbitrary upload commands - console.log("Starting"); + // UPLOAD mode to allow us to run arbitrary upload commands + console.log("Starting"); if (process.env.ENV_TYPE == "UPLOAD") { - run(); + run(); } else { const app = await NestFactory.create(AppModule); await app.listen(3050); } -} +} bootstrap(); diff --git a/apps/backend/src/users/user.service.ts b/apps/backend/src/users/user.service.ts index f750fca..7099991 100644 --- a/apps/backend/src/users/user.service.ts +++ b/apps/backend/src/users/user.service.ts @@ -27,7 +27,7 @@ export class UserService { // Determine what primary role should be used when searching, i.e. what permissions should take precedence let companyUserList: CompanyUsers[]; if (user.groups.includes(CognitoRoles.ADMIN)) { - throw new Error("Not Implemented") + throw new Error("Not Implemented"); //return this.getUsersForAdmin(); } else if (user.groups.includes(CognitoRoles.SUPERVISOR)) { companyUserList = await this.getUsersForSupervisor( @@ -49,7 +49,7 @@ export class UserService { ); } //return companyUserList; - throw new Error("Not Implemented") + throw new Error("Not Implemented"); } // TODO: this will need to do mapping to company data @@ -57,13 +57,13 @@ export class UserService { return this.getAllUsers(); } - /** + /** * Gets all the relevant users for a given Associate * @param user : The Associate who is requesting user information * @param companyIds : (optional) List of company IDs to restrict the search to. If empty, search all supervisor companies - * @param searchRoles : (optional) List of roles (associate, supervisor, admin) to restrict the search to. + * @param searchRoles : (optional) List of roles (associate, supervisor, admin) to restrict the search to. * @param userIds : (optional) List of user IDs to restrict the search to. - * @returns + * @returns */ async getUsersForAssociate( user: ValidatedUser, @@ -95,9 +95,9 @@ export class UserService { * Gets all the relevant users for a given Supervisor * @param user : The Supervisor who is requesting user information * @param companyIds : (optional) List of company IDs to restrict the search to. If empty, search all supervisor companies - * @param searchRoles : (optional) List of roles (associate, supervisor, admin) to restrict the search to. + * @param searchRoles : (optional) List of roles (associate, supervisor, admin) to restrict the search to. * @param userIds : (optional) List of user IDs to restrict the search to. - * @returns + * @returns */ async getUsersForSupervisor( user: ValidatedUser, @@ -145,8 +145,8 @@ export class UserService { console.log(cognitoUsers); // Include company information for each user - const usersWithCompanies = cognitoUsers.map(this.updateUserWithCompanyData) - console.log(usersWithCompanies) + const usersWithCompanies = cognitoUsers.map(this.updateUserWithCompanyData); + console.log(usersWithCompanies); return cognitoUsers; } @@ -294,7 +294,7 @@ export class UserService { ); var group = user.Attributes.find( (attribute) => attribute.Name === "cognito:groups" - ) + ); // TODO : refactor into separate 'getAttribute' function so we don't repeat this code return { diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index d3a584a..5c86a00 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -51,8 +51,8 @@ export class UsersController { } //return this.userService.getUsers(user, companyIds ?? [], roles, userIds); - throw new Error("Not implemented.") - } + throw new Error("Not implemented."); + } @Get("userById") public async getUserById( @@ -66,7 +66,7 @@ export class UsersController { HttpStatus.UNAUTHORIZED ); } - + // TODO: Throw error if no users found return this.userService.getUsersByIds(requester, userIds)[0]; } @@ -84,7 +84,7 @@ export class UsersController { ); } - return this.userService.getAllUsers() + return this.userService.getAllUsers(); } @Get("usersById") diff --git a/apps/backend/src/utils/decorators/user.decorator.ts b/apps/backend/src/utils/decorators/user.decorator.ts index 21a5f3a..faac3bc 100644 --- a/apps/backend/src/utils/decorators/user.decorator.ts +++ b/apps/backend/src/utils/decorators/user.decorator.ts @@ -1,4 +1,4 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { createParamDecorator, ExecutionContext } from "@nestjs/common"; /** * Creates a custom decorator that returns the request's user data based on the NestJS pipeline (e.g. authorization, roles guard, etc.) @@ -7,5 +7,5 @@ export const User = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.user; - }, -); \ No newline at end of file + } +); diff --git a/apps/backend/test/UploadTimesheet.ts b/apps/backend/test/UploadTimesheet.ts index 87b6c23..17b1e07 100644 --- a/apps/backend/test/UploadTimesheet.ts +++ b/apps/backend/test/UploadTimesheet.ts @@ -1,64 +1,77 @@ - -import { TimeSheetSchema, TimesheetStatus, TimesheetEntrySchema, CellType, TimeEntrySchema } from "../src/db/schemas/Timesheet" -import { v4 as uuidv4 } from 'uuid'; +import { + TimeSheetSchema, + TimesheetStatus, + TimesheetEntrySchema, + CellType, + TimeEntrySchema, +} from "../src/db/schemas/Timesheet"; +import { v4 as uuidv4 } from "uuid"; import { WriteEntryToTable } from "../src/dynamodb"; -//No idea why this require statement is needed but moment breaks otherwise :( -const moment = require('moment-timezone'); +//No idea why this require statement is needed but moment breaks otherwise :( +const moment = require("moment-timezone"); /*********************************************** Utils file used in testing to upload entire timesheets */ - - -const TIMEZONE = "America/New_York"; -const UUID = "4c8c5ad4-a8ab-4c92-b33f-b8f932b9e0b5" +const TIMEZONE = "America/New_York"; +const UUID = "4c8c5ad4-a8ab-4c92-b33f-b8f932b9e0b5"; function createTimeEntry(start, end) { - return TimeEntrySchema.parse({ - StartDateTime: start, - EndDateTime: end, - AuthorUUID: UUID - }); + return TimeEntrySchema.parse({ + StartDateTime: start, + EndDateTime: end, + AuthorUUID: UUID, + }); } function createEntry(cellType, date, associate, note) { - return TimesheetEntrySchema.parse({ - Type: cellType, - EntryID: uuidv4(), - Date: date, - AssociateTimes: associate, - SupervisorTimes: undefined, - AdminTimes: undefined, - Note: note - }); + return TimesheetEntrySchema.parse({ + Type: cellType, + EntryID: uuidv4(), + Date: date, + AssociateTimes: associate, + SupervisorTimes: undefined, + AdminTimes: undefined, + Note: note, + }); } -const current = moment().tz(TIMEZONE); +const current = moment().tz(TIMEZONE); -const daysOfWeek = moment().tz(TIMEZONE).startOf('week'); +const daysOfWeek = moment().tz(TIMEZONE).startOf("week"); const timesheetToUpload = TimeSheetSchema.parse({ - TimesheetID: Math.round(Math.random() * 1000000000), - UserID: UUID, - StartDate: moment().tz(TIMEZONE).startOf('week').day(0).unix(), - Status: TimesheetStatus.parse({ - HoursSubmitted: undefined, - HoursReviewed: undefined, - ScheduleSubmitted: undefined, - Finalized: undefined - }), - CompanyID: "Example Company 401", - HoursData: [ - createEntry(CellType.REGULAR, daysOfWeek.day(1).unix(), undefined, undefined), - createEntry(CellType.PTO, daysOfWeek.day(2).unix(), undefined, undefined), - createEntry(CellType.REGULAR, daysOfWeek.day(5).unix(), createTimeEntry(100, 500), undefined) - ], - ScheduleData: [], - WeekNotes: [] -}) + TimesheetID: Math.round(Math.random() * 1000000000), + UserID: UUID, + StartDate: moment().tz(TIMEZONE).startOf("week").day(0).unix(), + Status: TimesheetStatus.parse({ + HoursSubmitted: undefined, + HoursReviewed: undefined, + ScheduleSubmitted: undefined, + Finalized: undefined, + }), + CompanyID: "Example Company 401", + HoursData: [ + createEntry( + CellType.REGULAR, + daysOfWeek.day(1).unix(), + undefined, + undefined + ), + createEntry(CellType.PTO, daysOfWeek.day(2).unix(), undefined, undefined), + createEntry( + CellType.REGULAR, + daysOfWeek.day(5).unix(), + createTimeEntry(100, 500), + undefined + ), + ], + ScheduleData: [], + WeekNotes: [], +}); export function run() { - console.log("Uploading timesheet!"); - WriteEntryToTable(timesheetToUpload); - console.log("Success :)"); + console.log("Uploading timesheet!"); + WriteEntryToTable(timesheetToUpload); + console.log("Success :)"); } diff --git a/apps/frontend/README.md b/apps/frontend/README.md index 53fa78b..4f06a8e 100644 --- a/apps/frontend/README.md +++ b/apps/frontend/README.md @@ -1,73 +1,70 @@ ![](https://static.wixstatic.com/media/1193ef_371853f9145b445fb883f16ed7741b60~mv2.jpg/v1/crop/x_0,y_2,w_1842,h_332/fill/w_466,h_84,al_c,q_80,usm_0.66_1.00_0.01,enc_auto/Breaktime%20Logo%20Comfortaa-2.jpg) -# Breaktime Frontend +# Breaktime Frontend -This repo contains all the code for the backend of Project Breaktime, a project developing a time-tracking dashboard. This project utilizes a ReactJS (Frontend) and NodeJS (Backend). -## Installation +This repo contains all the code for the backend of Project Breaktime, a project developing a time-tracking dashboard. This project utilizes a ReactJS (Frontend) and NodeJS (Backend). -To setup this project ensure that you currently have react installed. +## Installation + +To setup this project ensure that you currently have react installed. [Install React](https://legacy.reactjs.org/docs/getting-started.html) -Once installed, all that is needed to install all required dependencies is the following line: +Once installed, all that is needed to install all required dependencies is the following line: + ``` npm install ``` -You should now be ready to start running things! -*Note*: If you are missing a file in `src/aws-exports.tsx` reach out to the tech-lead for this file, or copy `aws-exports.js` into this name. +You should now be ready to start running things! +_Note_: If you are missing a file in `src/aws-exports.tsx` reach out to the tech-lead for this file, or copy `aws-exports.js` into this name. ## Running the Project -To run the frontend several steps are required: - -1. - Run the following command: ```npm start``` After this you should see a browser open to `localhost:3000`. +To run the frontend several steps are required: -2. - Start the backend - for instructions see [breaktime-backend](https://github.com/Code-4-Community/breaktime-backend) under the c4c repo. Once this is running you should be able to start interacting with the website. +1. Run the following command: `npm start` After this you should see a browser open to `localhost:3000`. -3. - From step 1 you should be greeted with a log-in window asking you to sign in with a user name and password. These can either be provided by asking anyone on the breaktime team, or by navigating to the Cognito pool for breaktime and creating this yourself: *Ask a developer on project breaktime for instructions on this* +2. Start the backend - for instructions see [breaktime-backend](https://github.com/Code-4-Community/breaktime-backend) under the c4c repo. Once this is running you should be able to start interacting with the website. -Once logged in, you should be ready to go and interact with the website. +3. From step 1 you should be greeted with a log-in window asking you to sign in with a user name and password. These can either be provided by asking anyone on the breaktime team, or by navigating to the Cognito pool for breaktime and creating this yourself: _Ask a developer on project breaktime for instructions on this_ +Once logged in, you should be ready to go and interact with the website. -## Code Overview +## Code Overview -The frontend is using a typescript / react and currently utilizes AWS Amplify for handling authentication. +The frontend is using a typescript / react and currently utilizes AWS Amplify for handling authentication. -### Reading Material - -* [What is a frontend?](https://www.freecodecamp.org/news/front-end-developer-what-is-front-end-development-explained-in-plain-english/) -* [Introduction to AWS Amplify](https://docs.amplify.aws/) -* [Introduction to React](https://react.dev/learn) +### Reading Material +- [What is a frontend?](https://www.freecodecamp.org/news/front-end-developer-what-is-front-end-development-explained-in-plain-english/) +- [Introduction to AWS Amplify](https://docs.amplify.aws/) +- [Introduction to React](https://react.dev/learn) ### Codebase Design -Important directories and files are described below. While not all files are described, these provide a general overview of the structure. +Important directories and files are described below. While not all files are described, these provide a general overview of the structure. + ``` breaktime-frontend/ -├─ public/ - Static assets that should exist on the frontend -├─ src/ - Directory housing almost all code -│ ├─ components/ - React Component Modules -│ | ├─ Auth/ - Authenticated API interface and components -| | | ├─ apiClient.tsx - The central interface making authenticated calls to the backend. -│ | ├─ HomePage/ - Components for the landing & home page -│ | ├─ NavBar/ - Components for the navigation bar -│ | ├─ SignOut/ - Components for the signout page -│ | ├─ TimeCardPage/ - Components for the timesheet page -| | | ├─ CellTypes/ - Contains all types of cells rendered on the timesheet -│ ├─ schemas/ - Typescript schemas for complex data types -│ ├─ aws-exports.js - AWS configuration file for Cognito -│ ├─ constants.tsx - Frontend global constants file -│ ├─ index.css - styling for root of the project -│ ├─ index.tsx - root JS file -├─ .gitignore - files ignored by git -├─ package.json - react project configuration files +├─ public/ - Static assets that should exist on the frontend +├─ src/ - Directory housing almost all code +│ ├─ components/ - React Component Modules +│ | ├─ Auth/ - Authenticated API interface and components +| | | ├─ apiClient.tsx - The central interface making authenticated calls to the backend. +│ | ├─ HomePage/ - Components for the landing & home page +│ | ├─ NavBar/ - Components for the navigation bar +│ | ├─ SignOut/ - Components for the signout page +│ | ├─ TimeCardPage/ - Components for the timesheet page +| | | ├─ CellTypes/ - Contains all types of cells rendered on the timesheet +│ ├─ schemas/ - Typescript schemas for complex data types +│ ├─ aws-exports.js - AWS configuration file for Cognito +│ ├─ constants.tsx - Frontend global constants file +│ ├─ index.css - styling for root of the project +│ ├─ index.tsx - root JS file +├─ .gitignore - files ignored by git +├─ package.json - react project configuration files ├─ README.md ``` - diff --git a/apps/frontend/src/components/Auth/AuthWrapper.tsx b/apps/frontend/src/components/Auth/AuthWrapper.tsx index 61c07fc..59c40da 100644 --- a/apps/frontend/src/components/Auth/AuthWrapper.tsx +++ b/apps/frontend/src/components/Auth/AuthWrapper.tsx @@ -1,12 +1,6 @@ -import { Authenticator } from '@aws-amplify/ui-react'; -import React from 'react'; - - +import { Authenticator } from "@aws-amplify/ui-react"; +import React from "react"; export default function AuthedApp(props) { - return ( - - {props.page} - - ) + return {props.page}; } diff --git a/apps/frontend/src/components/HomePage/Announcements.tsx b/apps/frontend/src/components/HomePage/Announcements.tsx index 84d26d2..221c003 100644 --- a/apps/frontend/src/components/HomePage/Announcements.tsx +++ b/apps/frontend/src/components/HomePage/Announcements.tsx @@ -1,7 +1,18 @@ -import React, { useState } from 'react'; -import { Card, CardHeader, CardBody, Alert, Image, Button, Icon, Flex, VStack, Spacer } from '@chakra-ui/react'; -import { DEFAULT_COLORS } from 'src/constants'; -import { TfiAnnouncement } from 'react-icons/tfi'; +import React, { useState } from "react"; +import { + Card, + CardHeader, + CardBody, + Alert, + Image, + Button, + Icon, + Flex, + VStack, + Spacer, +} from "@chakra-ui/react"; +import { DEFAULT_COLORS } from "src/constants"; +import { TfiAnnouncement } from "react-icons/tfi"; const SAMPLE_EVENT_LIST = [ { @@ -29,26 +40,42 @@ export default function Announcements() { // }; return ( - - - - + + + + Announcements - + - {events[0] ? + {events[0] ? ( {events.map((event, index) => ( - - + + {`${event.date}: ${event.name}`} - - ))} + + + ))} - : 'No announcements'} + ) : ( + "No announcements" + )} diff --git a/apps/frontend/src/components/HomePage/HomePage.tsx b/apps/frontend/src/components/HomePage/HomePage.tsx index a28c8ed..728e969 100644 --- a/apps/frontend/src/components/HomePage/HomePage.tsx +++ b/apps/frontend/src/components/HomePage/HomePage.tsx @@ -1,17 +1,18 @@ -import { Grid } from '@chakra-ui/react'; -import Announcements from './Announcements'; -import Messages from './Messages'; -import MonthAtAGlance from './MonthAtAGlance'; -import React from 'react'; +import { Grid } from "@chakra-ui/react"; +import Announcements from "./Announcements"; +import Messages from "./Messages"; +import MonthAtAGlance from "./MonthAtAGlance"; +import React from "react"; export default function HomePage() { return ( + gridTemplateRows={"1fr 1 fr"} + gridTemplateColumns={"1fr 1fr"} + gap={"1%"} + > diff --git a/apps/frontend/src/components/HomePage/Messages.tsx b/apps/frontend/src/components/HomePage/Messages.tsx index 8d18e54..48bee45 100644 --- a/apps/frontend/src/components/HomePage/Messages.tsx +++ b/apps/frontend/src/components/HomePage/Messages.tsx @@ -1,7 +1,18 @@ -import React, { useState } from 'react'; -import { Card, CardHeader, CardBody, Alert, Button, CloseButton, Icon, Flex, VStack, Spacer } from '@chakra-ui/react'; -import { DEFAULT_COLORS } from 'src/constants'; -import { BiMessageDetail } from 'react-icons/bi'; +import React, { useState } from "react"; +import { + Card, + CardHeader, + CardBody, + Alert, + Button, + CloseButton, + Icon, + Flex, + VStack, + Spacer, +} from "@chakra-ui/react"; +import { DEFAULT_COLORS } from "src/constants"; +import { BiMessageDetail } from "react-icons/bi"; enum MessageTypes { Reminder = "Reminder", @@ -30,26 +41,42 @@ export default function Messages() { }; return ( - - - - + + + + Messages - + - {messages[0] ? + {messages[0] ? ( {messages.map((message, index) => ( - + deleteMessage(index)} /> - {message.type + ': ' + message.body} + {message.type + ": " + message.body} - - ))} + + + ))} - : 'No messages'} + ) : ( + "No messages" + )} diff --git a/apps/frontend/src/components/HomePage/MonthAtAGlance.tsx b/apps/frontend/src/components/HomePage/MonthAtAGlance.tsx index 31eba5b..0e453fc 100644 --- a/apps/frontend/src/components/HomePage/MonthAtAGlance.tsx +++ b/apps/frontend/src/components/HomePage/MonthAtAGlance.tsx @@ -1,8 +1,15 @@ -import React from 'react'; -import { Card, CardHeader, CardBody, Icon, Flex, Container } from '@chakra-ui/react'; -import { DEFAULT_COLORS } from 'src/constants'; -import { data, dataBar } from './dummyData'; -import { BsFillFileBarGraphFill } from 'react-icons/bs'; +import React from "react"; +import { + Card, + CardHeader, + CardBody, + Icon, + Flex, + Container, +} from "@chakra-ui/react"; +import { DEFAULT_COLORS } from "src/constants"; +import { data, dataBar } from "./dummyData"; +import { BsFillFileBarGraphFill } from "react-icons/bs"; import Chart from "chart.js/auto"; import { CategoryScale } from "chart.js"; @@ -12,24 +19,30 @@ Chart.register(CategoryScale); export default function MonthAtAGlance() { return ( - - - - + + + + Month at a Glance - + - - + + - + - ); + + ); } diff --git a/apps/frontend/src/components/NavBar/NavBar.tsx b/apps/frontend/src/components/NavBar/NavBar.tsx index c9f0c7a..fc3d4ea 100644 --- a/apps/frontend/src/components/NavBar/NavBar.tsx +++ b/apps/frontend/src/components/NavBar/NavBar.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { PAGE_ROUTES, DEFAULT_COLORS } from 'src/constants'; +import React from "react"; +import { PAGE_ROUTES, DEFAULT_COLORS } from "src/constants"; import { Box, Button, @@ -10,56 +10,74 @@ import { MenuList, MenuItem, Image, - ButtonGroup -} from '@chakra-ui/react' -import { - ChevronDownIcon -} from '@chakra-ui/icons'; + ButtonGroup, +} from "@chakra-ui/react"; +import { ChevronDownIcon } from "@chakra-ui/icons"; -const items = - [{ - "title": "Timesheets", - "link": PAGE_ROUTES.TIMECARD +const items = [ + { + title: "Timesheets", + link: PAGE_ROUTES.TIMECARD, }, { - "title": "Search", - "link": "" + title: "Search", + link: "", }, { - "title": "Home", - "link": PAGE_ROUTES.ROOT - }] + title: "Home", + link: PAGE_ROUTES.ROOT, + }, +]; function NavBar() { return ( - + - breaktime + breaktime - + - } rounded={'md'} bgColor={DEFAULT_COLORS.BREAKTIME_BLUE} textColor={DEFAULT_COLORS.WHITE}> + } + rounded={"md"} + bgColor={DEFAULT_COLORS.BREAKTIME_BLUE} + textColor={DEFAULT_COLORS.WHITE} + > Menu - {items.map( - (dropDownItem) => - ( - - {dropDownItem.title} - - ) - )} + {items.map((dropDownItem) => ( + + {dropDownItem.title} + + ))} - - ) + ); } export default NavBar; diff --git a/apps/frontend/src/components/SignOut/Signout.tsx b/apps/frontend/src/components/SignOut/Signout.tsx index 1a8aa36..bb66b51 100644 --- a/apps/frontend/src/components/SignOut/Signout.tsx +++ b/apps/frontend/src/components/SignOut/Signout.tsx @@ -1,13 +1,8 @@ - -import React from 'react'; -import ApiClient from '../Auth/apiClient'; -import { Heading } from '@chakra-ui/react'; +import React from "react"; +import ApiClient from "../Auth/apiClient"; +import { Heading } from "@chakra-ui/react"; export default function HomePage() { - ApiClient.signout() - return ( - - Logged out - - ); + ApiClient.signout(); + return Logged out; } diff --git a/apps/frontend/src/components/TimeCardPage/AggregationTable.tsx b/apps/frontend/src/components/TimeCardPage/AggregationTable.tsx index 27629a1..7fae0bf 100644 --- a/apps/frontend/src/components/TimeCardPage/AggregationTable.tsx +++ b/apps/frontend/src/components/TimeCardPage/AggregationTable.tsx @@ -1,92 +1,77 @@ -import { - Table, - Thead, - Tbody, - Tfoot, - Tr, - Th, - Td -} from '@chakra-ui/react'; -import React from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import moment, { Moment } from 'moment-timezone'; -import { TimeSheetSchema } from '../../schemas/TimesheetSchema' +import { Table, Thead, Tbody, Tfoot, Tr, Th, Td } from "@chakra-ui/react"; +import React from "react"; +import { v4 as uuidv4 } from "uuid"; +import moment, { Moment } from "moment-timezone"; +import { TimeSheetSchema } from "../../schemas/TimesheetSchema"; interface AggregationProps { - Date: Moment, - timesheets: TimeSheetSchema[] + Date: Moment; + timesheets: TimeSheetSchema[]; } - function AggregationTable(props: AggregationProps) { - //NOTE: Aggregation is only applying to associate entries currently - TODO is to develop logic for all user types + //NOTE: Aggregation is only applying to associate entries currently - TODO is to develop logic for all user types - const totalHoursForEachDay = {}; + const totalHoursForEachDay = {}; - // add the days in that stretch to dictionary - // set all to 0 - // iterate through each sheet and increment accordingly + // add the days in that stretch to dictionary + // set all to 0 + // iterate through each sheet and increment accordingly - const finalDate = moment(props.Date).add(7, 'days'); - const currentDate = moment(props.Date); - while (currentDate.isBefore(finalDate, 'days')) { - totalHoursForEachDay[currentDate.format("MM/DD/YY")] = 0; - currentDate.add(1, 'day'); - //console.log("Date: ", currentDate.format("MM/DD/YY")); - } - props.timesheets.forEach(sheet => { - if (sheet.TableData !== undefined) { - sheet.TableData.forEach(entry => { - if (entry.Associate !== undefined && entry.Associate.Start !== undefined && entry.Associate.End !== undefined) { - totalHoursForEachDay[moment.unix(entry.Date).format("MM/DD/YY")] += Number(entry.Associate.End - entry.Associate.Start); - } - totalHoursForEachDay[moment.unix(entry.Date).format("MM/DD/YY")] += 0; - }); - } - }); + const finalDate = moment(props.Date).add(7, "days"); + const currentDate = moment(props.Date); + while (currentDate.isBefore(finalDate, "days")) { + totalHoursForEachDay[currentDate.format("MM/DD/YY")] = 0; + currentDate.add(1, "day"); + //console.log("Date: ", currentDate.format("MM/DD/YY")); + } + props.timesheets.forEach((sheet) => { + if (sheet.TableData !== undefined) { + sheet.TableData.forEach((entry) => { + if ( + entry.Associate !== undefined && + entry.Associate.Start !== undefined && + entry.Associate.End !== undefined + ) { + totalHoursForEachDay[moment.unix(entry.Date).format("MM/DD/YY")] += + Number(entry.Associate.End - entry.Associate.Start); + } + totalHoursForEachDay[moment.unix(entry.Date).format("MM/DD/YY")] += 0; + }); + } + }); - const aggregatedRows = Object.entries(totalHoursForEachDay).map(entry => - ({ - "Date": entry[0], - "Duration": Number(entry[1]) - })); + const aggregatedRows = Object.entries(totalHoursForEachDay).map((entry) => ({ + Date: entry[0], + Duration: Number(entry[1]), + })); - const totalHours = aggregatedRows.reduce((acc, row) => acc + row.Duration, 0); + const totalHours = aggregatedRows.reduce((acc, row) => acc + row.Duration, 0); - return ( - - - - - - - - - {aggregatedRows.map( - (totalRow) => { - return ( - - - - - ) - } - )} - - - - - -
DateHours
- {totalRow.Date} - - {(totalRow.Duration / 60).toFixed(2)} -
- Total Hours - - {(totalHours / 60).toFixed(2)} -
- ); + return ( + + + + + + + + + {aggregatedRows.map((totalRow) => { + return ( + + + + + ); + })} + + + + + +
DateHours
{totalRow.Date}{(totalRow.Duration / 60).toFixed(2)}
Total Hours{(totalHours / 60).toFixed(2)}
+ ); } export default AggregationTable; diff --git a/apps/frontend/src/components/TimeCardPage/CellTypes/CommentCell.tsx b/apps/frontend/src/components/TimeCardPage/CellTypes/CommentCell.tsx index 28d66a4..a60462d 100644 --- a/apps/frontend/src/components/TimeCardPage/CellTypes/CommentCell.tsx +++ b/apps/frontend/src/components/TimeCardPage/CellTypes/CommentCell.tsx @@ -26,7 +26,15 @@ export function CommentCell({ getAllActiveCommentsOfType(CommentType.Comment, comments) ); const [reports, setReports] = useState( - getAllActiveCommentsOfType(CommentType.Report, comments) as ReportSchema[] + getAllActiveCommentsOfType(CommentType.Report, comments).map((comment) => ({ + AuthorID: comment.AuthorID, + Type: comment.Type, + Content: comment.Content.split(",")[0], + Notified: comment.Content.split(",")[1], + Explanation: comment.Content.split(",")[2], + State: comment.State, + Timestamp: comment.Timestamp, + })) as ReportSchema[] ); const [isEditable, setisEditable] = useState(false); const user = useContext(UserContext); @@ -40,7 +48,17 @@ export function CommentCell({ const updateReports = (updatedReports: ReportSchema[]) => { setReports(updatedReports); - updateComments("Comment", currentComments.concat(updatedReports)); + + const reportsToComments = updatedReports.map((report) => ({ + UUID: report.AuthorID, + AuthorID: report.AuthorID, + Type: report.Type, + Timestamp: report.Timestamp, + Content: `${report.Content},${report.Notified},${report.Explanation}`, + State: report.State, + })) as CommentSchema[]; + + updateComments("Comment", currentComments.concat(reportsToComments)); }; return ( diff --git a/apps/frontend/src/components/TimeCardPage/CellTypes/CommentModals/ShowCommentModal.tsx b/apps/frontend/src/components/TimeCardPage/CellTypes/CommentModals/ShowCommentModal.tsx index 02265b6..ee86394 100644 --- a/apps/frontend/src/components/TimeCardPage/CellTypes/CommentModals/ShowCommentModal.tsx +++ b/apps/frontend/src/components/TimeCardPage/CellTypes/CommentModals/ShowCommentModal.tsx @@ -24,7 +24,7 @@ import { Text, IconButton, Textarea, - useToast + useToast, } from "@chakra-ui/react"; import { @@ -33,7 +33,7 @@ import { CloseIcon, EditIcon, AddIcon, - DeleteIcon + DeleteIcon, } from "@chakra-ui/icons"; import { CommentSchema } from "../../../../schemas/RowSchema"; @@ -43,229 +43,279 @@ import apiClient from "src/components/Auth/apiClient"; import { createToast } from "../../utils"; const saveEditedComment = ( - setComments: Function, - comments: CommentSchema[], - typeOfComment: CommentType, - prevComment: CommentSchema, - newComment: CommentSchema) => { + setComments: Function, + comments: CommentSchema[], + typeOfComment: CommentType, + prevComment: CommentSchema, + newComment: CommentSchema +) => { // previous comment edited over so set it to deleted - prevComment.State = CellStatus.Deleted - setComments(getAllActiveCommentsOfType(typeOfComment, [...comments, newComment])); + prevComment.State = CellStatus.Deleted; + setComments( + getAllActiveCommentsOfType(typeOfComment, [...comments, newComment]) + ); // TODO: save to DB }; const deleteComment = ( - onCloseDisplay: Function, - setComments: Function, - comments: CommentSchema[], - typeOfComment: CommentType, - comment: CommentSchema) => { + onCloseDisplay: Function, + setComments: Function, + comments: CommentSchema[], + typeOfComment: CommentType, + comment: CommentSchema +) => { // TODO: add confirmation popup - comment.State = CellStatus.Deleted + comment.State = CellStatus.Deleted; setComments(getAllActiveCommentsOfType(typeOfComment, comments)); if (comments.length === 1) { - onCloseDisplay() + onCloseDisplay(); } // TODO: save to DB -} +}; interface ShowCommentModalProps { - comments: CommentSchema[]; - setComments: Function; - isEditable: boolean; - timesheetID: number; - } - + comments: CommentSchema[]; + setComments: Function; + isEditable: boolean; + timesheetID: number; +} + export default function ShowCommentModal({ - comments, - setComments, - isEditable, - timesheetID - }: ShowCommentModalProps) { - const { isOpen: isOpenDisplay, onOpen: onOpenDisplay, onClose: onCloseDisplay } = useDisclosure(); - const { isOpen: isOpenAdd, onOpen: onOpenAdd, onClose: onCloseAdd } = useDisclosure(); - const user = useContext(UserContext); - let color = Color.Blue - - const EditableControls = () => { - const { - isEditing, - getSubmitButtonProps, - getCancelButtonProps, - getEditButtonProps, - } = useEditableControls(); - - // TODO: change this later to reflect figma - return isEditing ? ( - - + + + + + {comments.map((comment) => ( - View {CommentType.Comment} - + {/* add UserDisplay card once pr merged in*/} + + saveEditedComment( + setComments, + comments, + CommentType.Comment, + comment, + createNewComment(user, CommentType.Comment, value) + ) + } + > + + + {isEditable && ( + <> + + + + } + onClick={() => + deleteComment( + onCloseDisplay, + setComments, + comments, + CommentType.Comment, + comment + ) + } + /> + + + )} + - - - - {comments.map( - (comment) => ( - - {/* add UserDisplay card once pr merged in*/} - saveEditedComment(setComments, comments, CommentType.Comment, comment, createNewComment(user, CommentType.Comment, value))} - > - - - {isEditable && ( - <> - - - - } onClick={() => deleteComment(onCloseDisplay, setComments, comments, CommentType.Comment, comment)} /> - - - )} - - - - ))} - - - - - - - ) - } - - const AddCommentModal = () => { - const [remark, setRemark] = useState(); - const user = useContext(UserContext); - const toast = useToast(); - - const handleRemarkChange = (e) => { - setRemark(e.target.value); - }; - - const handleSubmit = () => { - // TODO: reuse comment validation - setComments([...comments, createNewComment(user, CommentType.Comment, remark)]); - apiClient.saveComment(remark, timesheetID).then((resp) => - {if (resp) { - toast(createToast({position: 'bottom-right',title:'success.', description: "Your report has been saved.", status: "success"})) + ))} + + + + + + ); + }; + + const AddCommentModal = () => { + const [remark, setRemark] = useState(); + const user = useContext(UserContext); + const toast = useToast(); + + const handleRemarkChange = (e) => { + setRemark(e.target.value); + }; + + const handleSubmit = () => { + // TODO: reuse comment validation + setComments([ + ...comments, + createNewComment(user, CommentType.Comment, remark), + ]); + apiClient + .saveComment(remark, timesheetID) + .then((resp) => { + if (resp) { + toast( + createToast({ + position: "bottom-right", + title: "success.", + description: "Your report has been saved.", + status: "success", + }) + ); } else { - toast(createToast({ - position: 'bottom-right', - title: 'failed', + toast( + createToast({ + position: "bottom-right", + title: "failed", + description: "An error occured. Please try again.", + status: "error", + duration: 9000, + isClosable: true, + }) + ); + } + }) + .catch((err) => + toast( + createToast({ + position: "bottom-right", + title: "failed", description: "An error occured. Please try again.", - status: 'error', + status: "error", duration: 9000, isClosable: true, - })) - }} - ).catch((err) => - toast(createToast({ - position: 'bottom-right', - title: 'failed', - description: "An error occured. Please try again.", - status: 'error', - duration: 9000, - isClosable: true, - }))) - onCloseAdd() - }; - - return ( - - - }> - {CommentType.Comment} - -
- - -