diff --git a/package-lock.json b/package-lock.json index ed756cc..e1f4abf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@faker-js/faker": "^8.4.1", "@fontsource/inter": "^5.0.16", "@fontsource/plus-jakarta-sans": "^5.0.18", + "@fullcalendar/react": "^6.1.11", "@mui/icons-material": "^5.15.0", "@mui/material": "^5.15.0", "@mui/material-nextjs": "^5.15.0", @@ -21,13 +22,14 @@ "@mui/x-date-pickers": "^6.18.7", "@unly/universal-language-detector": "^2.0.3", "apexcharts": "^3.45.2", - "axios": "^1.6.8", + "date-holidays": "^3.23.11", "dayjs": "^1.11.10", + "fullcalendar": "^6.1.11", "i18next": "^23.7.11", "i18next-resources-to-backend": "^1.2.0", "lodash": "^4.17.21", "next": "14.0.4", - "next-auth": "^4.24.5", + "next-auth": "^4.24.7", "next-pwa": "^5.6.0", "react": "^18", "react-apexcharts": "^1.4.1", @@ -2081,6 +2083,79 @@ "tslib": "^2.4.0" } }, + "node_modules/@fullcalendar/core": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.11.tgz", + "integrity": "sha512-TjG7c8sUz+Vkui2FyCNJ+xqyu0nq653Ibe99A66LoW95oBo6tVhhKIaG1Wh0GVKymYiqAQN/OEdYTuj4ay27kA==", + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/core/node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.11.tgz", + "integrity": "sha512-hF5jJB7cgUIxWD5MVjj8IU407HISyLu7BWXcEIuTytkfr8oolOXeCazqnnjmRbnFOncoJQVstTtq6SIhaT32Xg==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + } + }, + "node_modules/@fullcalendar/interaction": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.11.tgz", + "integrity": "sha512-ynOKjzuPwEAMgTQ6R/Z2zvzIIqG4p8/Qmnhi1q0vzPZZxSIYx3rlZuvpEK2WGBZZ1XEafDOP/LGfbWoNZe+qdg==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + } + }, + "node_modules/@fullcalendar/list": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.11.tgz", + "integrity": "sha512-9Qx8uvik9pXD12u50FiHwNzlHv4wkhfsr+r03ycahW7vEeIAKCsIZGTkUfFP+96I5wHihrfLazu1cFQG4MPiuw==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + } + }, + "node_modules/@fullcalendar/multimonth": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@fullcalendar/multimonth/-/multimonth-6.1.11.tgz", + "integrity": "sha512-7DbPC+AAlaKnquGVdw1Z85Q3nSZ4GZ1NcVIk4k7bLnqDlntwHPPsrDlSIzUWKcN0q5/u7jQHm4PU1m3LAl70Sg==", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.11" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + } + }, + "node_modules/@fullcalendar/react": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.11.tgz", + "integrity": "sha512-Og0Tv0OiglTFp+b++yRyEhAeWnAmKkMLQ3iS0eJE1KDEov6QqGkoO+dUG4x8zp2w55IJqzik/a9iHi0s3oQDbA==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11", + "react": "^16.7.0 || ^17 || ^18", + "react-dom": "^16.7.0 || ^17 || ^18" + } + }, + "node_modules/@fullcalendar/timegrid": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.11.tgz", + "integrity": "sha512-0seUHK/ferH89IeuCvV4Bib0zWjgK0nsptNdmAc9wDBxD/d9hm5Mdti0URJX6bDoRtsSfRDu5XsRcrzwoc+AUQ==", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.11" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3648,8 +3723,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { "version": "5.3.0", @@ -3855,6 +3929,14 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/astronomia": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/astronomia/-/astronomia-4.1.1.tgz", + "integrity": "sha512-TcJD9lUC5eAo0/Ji7rnQauX/yQbi0yZWM+JsNr77W3OA5fsrgvuFgubLMFwfw4VlZ29cu9dG/yfJbfvuTSftjg==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", @@ -3869,11 +3951,6 @@ "has-symbols": "^1.0.3" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -3942,16 +4019,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -4136,6 +4203,17 @@ "node": ">=10.16.0" } }, + "node_modules/caldate": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/caldate/-/caldate-2.0.5.tgz", + "integrity": "sha512-JndhrUuDuE975KUhFqJaVR1OQkCHZqpOrJur/CFXEIEhWhBMjxO85cRSK8q4FW+B+yyPq6GYua2u4KvNzTcq0w==", + "dependencies": { + "moment-timezone": "^0.5.43" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -4337,17 +4415,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4489,6 +4556,68 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/date-bengali-revised": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/date-bengali-revised/-/date-bengali-revised-2.0.2.tgz", + "integrity": "sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/date-chinese": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/date-chinese/-/date-chinese-2.1.4.tgz", + "integrity": "sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==", + "dependencies": { + "astronomia": "^4.1.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/date-easter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/date-easter/-/date-easter-1.0.3.tgz", + "integrity": "sha512-aOViyIgpM4W0OWUiLqivznwTtuMlD/rdUWhc5IatYnplhPiWrLv75cnifaKYhmQwUBLAMWLNG4/9mlLIbXoGBQ==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/date-holidays": { + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/date-holidays/-/date-holidays-3.23.12.tgz", + "integrity": "sha512-DLyP0PPVgNydgaTAY7SBS26+5h3KO1Z8FRKiAROkz0hAGNBLGAM48SMabfVa2ACRHH7Qw3LXYvlJkt9oa9WePA==", + "dependencies": { + "date-holidays-parser": "^3.4.4", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "prepin": "^1.0.3" + }, + "bin": { + "holidays2json": "scripts/holidays2json.cjs" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/date-holidays-parser": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/date-holidays-parser/-/date-holidays-parser-3.4.4.tgz", + "integrity": "sha512-R5aO4oT8H51ZKdvApqHrqYEiNBrqT6tRj2PFXNcZfqMI4nxY7KKKly0ZsmquR5gY+x9ldKR8SAMdozzIInaoXg==", + "dependencies": { + "astronomia": "^4.1.1", + "caldate": "^2.0.5", + "date-bengali-revised": "^2.0.2", + "date-chinese": "^2.1.4", + "date-easter": "^1.0.2", + "deepmerge": "^4.3.1", + "jalaali-js": "^1.2.6", + "moment-timezone": "^0.5.43" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", @@ -4626,14 +4755,6 @@ "rimraf": "bin.js" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -5706,25 +5827,6 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -5802,6 +5904,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fullcalendar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-6.1.11.tgz", + "integrity": "sha512-OOlx/+yFn9k5LnucRzcDmShONBecOVKNN6HHWe8jl7hGzQBmkxO+iD6eBokO6p24EY1PjATqhZkhJqHiCUgx3A==", + "dependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar/daygrid": "~6.1.11", + "@fullcalendar/interaction": "~6.1.11", + "@fullcalendar/list": "~6.1.11", + "@fullcalendar/multimonth": "~6.1.11", + "@fullcalendar/timegrid": "~6.1.11" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6755,6 +6870,11 @@ "node": ">=8" } }, + "node_modules/jalaali-js": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.6.tgz", + "integrity": "sha512-io974va+Qyu+UfuVX3UIAgJlxLhAMx9Y8VMfh+IG00Js7hXQo1qNQuwSiSa0xxco0SVgx5HWNkaiCcV+aZ8WPw==" + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -6800,9 +6920,9 @@ } }, "node_modules/jose": { - "version": "4.15.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", - "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -6816,7 +6936,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -7135,6 +7254,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "peer": true, "engines": { "node": ">= 0.6" } @@ -7143,6 +7263,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -7183,6 +7304,25 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7275,14 +7415,14 @@ } }, "node_modules/next-auth": { - "version": "4.24.6", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.6.tgz", - "integrity": "sha512-djQt3ZEaWEIxcsuh3HTW2uuzLfXMRjHH+ugAsichlQSbH4iA5MRcgMA2HvTNvsDTDLh44tyU72+/gWsxgTbAKg==", + "version": "4.24.7", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.7.tgz", + "integrity": "sha512-iChjE8ov/1K/z98gdKbn2Jw+2vLgJtVV39X+rCP5SGnVQuco7QOr19FRNGMIrD8d3LYhHWV9j9sKLzq1aDWWQQ==", "dependencies": { "@babel/runtime": "^7.20.13", "@panva/hkdf": "^1.0.2", "cookie": "^0.5.0", - "jose": "^4.11.4", + "jose": "^4.15.5", "oauth": "^0.9.15", "openid-client": "^5.4.0", "preact": "^10.6.3", @@ -7993,6 +8133,14 @@ "node": ">= 0.8.0" } }, + "node_modules/prepin": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/prepin/-/prepin-1.0.3.tgz", + "integrity": "sha512-0XL2hreherEEvUy0fiaGEfN/ioXFV+JpImqIzQjxk6iBg4jQ2ARKqvC4+BmRD8w/pnpD+lbxvh0Ub+z7yBEjvA==", + "bin": { + "prepin": "bin/prepin.js" + } + }, "node_modules/prettier": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", @@ -8057,11 +8205,6 @@ "react-is": "^16.13.1" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/src/app/(main)/settings/skill/edit/page.tsx b/src/app/(main)/settings/skill/edit/page.tsx new file mode 100644 index 0000000..a2d9863 --- /dev/null +++ b/src/app/(main)/settings/skill/edit/page.tsx @@ -0,0 +1,28 @@ +import { Edit } from "@mui/icons-material"; +import { useSearchParams } from "next/navigation"; +// import EditStaff from "@/components/EditStaff"; +import { Suspense } from "react"; +import { I18nProvider, getServerI18n } from "@/i18n"; +// import EditStaffWrapper from "@/components/EditStaff/EditStaffWrapper"; +import { Metadata } from "next"; +import EditSkill from "@/components/EditSkill"; +import { Typography } from "@mui/material"; + + +const EditSkillPage: React.FC = async () => { + const { t } = await getServerI18n("staff"); + + return ( + <> + {t("Edit Skill")} + + }> + + + + {/* */} + + ); +}; + +export default EditSkillPage; diff --git a/src/app/(main)/tasks/edit/not-found.tsx b/src/app/(main)/tasks/edit/not-found.tsx new file mode 100644 index 0000000..b99e85e --- /dev/null +++ b/src/app/(main)/tasks/edit/not-found.tsx @@ -0,0 +1,17 @@ +import { getServerI18n } from "@/i18n"; +import { Stack, Typography, Link } from "@mui/material"; +import NextLink from "next/link"; + +export default async function NotFound() { + const { t } = await getServerI18n("tasks", "common"); + + return ( + + {t("Not Found")} + {t("The task template was not found!")} + + {t("Return to all task templates")} + + + ); +} diff --git a/src/app/(main)/tasks/edit/page.tsx b/src/app/(main)/tasks/edit/page.tsx index 2b2c02c..4872daf 100644 --- a/src/app/(main)/tasks/edit/page.tsx +++ b/src/app/(main)/tasks/edit/page.tsx @@ -1,23 +1,44 @@ -import { preloadAllTasks } from "@/app/api/tasks"; +import { fetchTaskTemplate, preloadAllTasks } from "@/app/api/tasks"; import CreateTaskTemplate from "@/components/CreateTaskTemplate"; import { getServerI18n } from "@/i18n"; import Typography from "@mui/material/Typography"; import { Metadata } from "next"; import { I18nProvider } from "@/i18n"; +import { ServerFetchError } from "@/app/utils/fetchUtil"; +import { isArray } from "lodash"; +import { notFound } from "next/navigation"; export const metadata: Metadata = { title: "Edit Task Template", }; -const TaskTemplates: React.FC = async () => { +interface Props { + searchParams: { [key: string]: string | string[] | undefined }; +} + +const TaskTemplates: React.FC = async ({ searchParams }) => { const { t } = await getServerI18n("tasks"); + const taskTemplateId = searchParams["id"]; + + if (!taskTemplateId || isArray(taskTemplateId)) { + notFound(); + } + preloadAllTasks(); + try { + await fetchTaskTemplate(taskTemplateId); + } catch (e) { + if (e instanceof ServerFetchError && e.response?.status === 404) { + notFound(); + } + } + return ( <> {t("Edit Task Template")} - + ); diff --git a/src/app/api/group/actions.ts b/src/app/api/group/actions.ts index 204c1d9..6800b29 100644 --- a/src/app/api/group/actions.ts +++ b/src/app/api/group/actions.ts @@ -29,8 +29,8 @@ export interface record { records: auth[]; } -export const fetchAuth = cache(async (id?: number) => { - return serverFetchJson(`${BASE_API_URL}/group/auth/combo/${id ?? 0}`, { +export const fetchAuth = cache(async (target: string, id?: number) => { + return serverFetchJson(`${BASE_API_URL}/group/auth/${target}/${id ?? 0}`, { next: { tags: ["auth"] }, }); }); diff --git a/src/app/api/tasks/actions.ts b/src/app/api/tasks/actions.ts index 2c043be..3414bf6 100644 --- a/src/app/api/tasks/actions.ts +++ b/src/app/api/tasks/actions.ts @@ -4,13 +4,26 @@ import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil import { BASE_API_URL } from "@/config/api"; import { TaskTemplate } from "."; import { revalidateTag } from "next/cache"; +import { ManhourAllocation } from "@/app/api/projects/actions"; +import { Task, TaskGroup } from '@/app/api/tasks'; export interface NewTaskTemplateFormInputs { + + // task template code: string; name: string; taskIds: number[]; id: number | null; + + // resource allocation template + manhourPercentageByGrade: ManhourAllocation; + taskGroups: { + [taskGroup: TaskGroup["id"]]: { + taskIds: Task["id"][]; + percentAllocation: number; + }; + }; } export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => { diff --git a/src/app/api/tasks/index.ts b/src/app/api/tasks/index.ts index f701cca..ba993f0 100644 --- a/src/app/api/tasks/index.ts +++ b/src/app/api/tasks/index.ts @@ -39,3 +39,15 @@ export const preloadAllTasks = () => { export const fetchAllTasks = cache(async () => { return serverFetchJson(`${BASE_API_URL}/tasks`); }); + +export const fetchTaskTemplate = cache(async (id: string) => { + const taskTemplate = await serverFetchJson( + `${BASE_API_URL}/tasks/templatesDetails/${id}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + return taskTemplate; +}); diff --git a/src/app/api/timesheets/actions.ts b/src/app/api/timesheets/actions.ts index c836e6e..cd32d06 100644 --- a/src/app/api/timesheets/actions.ts +++ b/src/app/api/timesheets/actions.ts @@ -11,7 +11,8 @@ export interface TimeEntry { projectId?: ProjectResult["id"]; taskGroupId?: TaskGroup["id"]; taskId?: Task["id"]; - inputHours: number; + inputHours?: number; + otHours?: number; remark?: string; } diff --git a/src/app/api/timesheets/utils.ts b/src/app/api/timesheets/utils.ts index 68ee075..c305e26 100644 --- a/src/app/api/timesheets/utils.ts +++ b/src/app/api/timesheets/utils.ts @@ -8,19 +8,24 @@ export const isValidTimeEntry = (entry: Partial): string => { // Test for errors let error: keyof TimeEntry | "" = ""; + // Either normal or other hours need to be inputted + if (!entry.inputHours && !entry.otHours) { + error = "inputHours"; + } else if (entry.inputHours && entry.inputHours <= 0) { + error = "inputHours"; + } else if (entry.otHours && entry.otHours <= 0) { + error = "otHours"; + } + // If there is a project id, there should also be taskGroupId, taskId, inputHours if (entry.projectId) { if (!entry.taskGroupId) { error = "taskGroupId"; } else if (!entry.taskId) { error = "taskId"; - } else if (!entry.inputHours || !(entry.inputHours >= 0)) { - error = "inputHours"; } } else { - if (!entry.inputHours || !(entry.inputHours >= 0)) { - error = "inputHours"; - } else if (!entry.remark) { + if (!entry.remark) { error = "remark"; } } diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts index fff3da4..77b58b5 100644 --- a/src/app/api/user/actions.ts +++ b/src/app/api/user/actions.ts @@ -7,8 +7,10 @@ import { UserDetail, UserResult } from "."; import { cache } from "react"; export interface UserInputs { - username: string; - email: string; + name: string; + email?: string; + addAuthIds?: number[]; + removeAuthIds?: number[]; } export interface PasswordInputs { @@ -40,7 +42,7 @@ export const deleteUser = async (id: number) => { }; export const changePassword = async (data: any) => { - return serverFetchJson(`${BASE_API_URL}/user/change-password`, { + return serverFetchWithNoContent(`${BASE_API_URL}/user/change-password`, { method: "PATCH", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, diff --git a/src/app/api/user/index.ts b/src/app/api/user/index.ts index 3151b64..f34292f 100644 --- a/src/app/api/user/index.ts +++ b/src/app/api/user/index.ts @@ -3,7 +3,6 @@ import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; import "server-only"; - export interface UserResult { action: any; id: number; @@ -20,6 +19,7 @@ export interface UserResult { phone2: string; remarks: string; groupId: number; + auths: any } // export interface DetailedUser extends UserResult { @@ -28,9 +28,10 @@ export interface UserResult { // } export interface UserDetail { - authIds: number[]; data: UserResult; + authIds: number[]; groupIds: number[]; + auths: any[] } export const preloadUser = () => { diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 4166be0..a8a13a8 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -144,22 +144,48 @@ const CreateProject: React.FC = ({ // detect errors let hasErrors = false + + // Tab - Staff Allocation and Resource if (data.totalManhour === null || data.totalManhour <= 0) { formProps.setError("totalManhour", { message: "totalManhour value is not valid", type: "required" }) + setTabIndex(2) hasErrors = true } const manhourPercentageByGradeKeys = Object.keys(data.manhourPercentageByGrade) if (manhourPercentageByGradeKeys.filter(k => data.manhourPercentageByGrade[k as any] < 0).length > 0 || - manhourPercentageByGradeKeys.reduce((acc, value) => acc + data.manhourPercentageByGrade[value as any], 0) !== 1) { + manhourPercentageByGradeKeys.reduce((acc, value) => acc + data.manhourPercentageByGrade[value as any], 0) !== 100) { formProps.setError("manhourPercentageByGrade", { message: "manhourPercentageByGrade value is not valid", type: "invalid" }) + setTabIndex(2) hasErrors = true } const taskGroupKeys = Object.keys(data.taskGroups) if (taskGroupKeys.filter(k => data.taskGroups[k as any].percentAllocation < 0).length > 0 || - taskGroupKeys.reduce((acc, value) => acc + data.taskGroups[value as any].percentAllocation, 0) !== 1) { - formProps.setError("taskGroups", {message: "Task Groups value is not invalid", type: "invalid"}) + taskGroupKeys.reduce((acc, value) => acc + data.taskGroups[value as any].percentAllocation, 0) !== 100) { + formProps.setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" }) + setTabIndex(2) + hasErrors = true + } + + // Tab - Milestone + let projectTotal = 0 + const milestonesKeys = Object.keys(data.milestones) + milestonesKeys.forEach(key => { + const { startDate, endDate, payments } = data.milestones[parseFloat(key)] + + if (!Boolean(startDate) || startDate === "Invalid Date" || !Boolean(endDate) || endDate === "Invalid Date" || new Date(startDate) > new Date(endDate)) { + formProps.setError("milestones", {message: "milestones is not valid", type: "invalid"}) + setTabIndex(3) + hasErrors = true + } + + projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0) + }) + + if (projectTotal !== data.expectedProjectFee) { + formProps.setError("milestones", {message: "milestones is not valid", type: "invalid"}) + setTabIndex(3) hasErrors = true } @@ -245,7 +271,7 @@ const CreateProject: React.FC = ({ // manhourPercentageByGrade should have a sensible default manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade) ? grades.reduce((acc, grade) => { - return { ...acc, [grade.id]: 1 / grades.length }; + return { ...acc, [grade.id]: 100 / grades.length }; }, {}) : defaultInputs?.manhourPercentageByGrade, }, diff --git a/src/components/CreateProject/Milestone.tsx b/src/components/CreateProject/Milestone.tsx index d59990d..66dcb2d 100644 --- a/src/components/CreateProject/Milestone.tsx +++ b/src/components/CreateProject/Milestone.tsx @@ -4,7 +4,7 @@ import Card from "@mui/material/Card"; import CardContent from "@mui/material/CardContent"; import { useTranslation } from "react-i18next"; import Button from "@mui/material/Button"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import CardActions from "@mui/material/CardActions"; import RestartAlt from "@mui/icons-material/RestartAlt"; import { @@ -29,7 +29,7 @@ export interface Props { const Milestone: React.FC = ({ allTasks, isActive }) => { const { t } = useTranslation(); - const { watch } = useFormContext(); + const { watch, setError, clearErrors } = useFormContext(); const currentTaskGroups = watch("taskGroups"); const taskGroups = useMemo( () => @@ -57,6 +57,35 @@ const Milestone: React.FC = ({ allTasks, isActive }) => { [], ); + // handle error checking + const milestones = watch("milestones") + const expectedTotalFee = watch("expectedProjectFee"); + useEffect(() => { + const milestonesKeys = Object.keys(milestones) + let hasError = false + let projectTotal = 0 + + milestonesKeys.forEach(key => { + const { startDate, endDate, payments } = milestones[parseFloat(key)] + + if (new Date(startDate) > new Date(endDate) || !Boolean(startDate) || !Boolean(endDate)) { + hasError = true + } + + projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0) + }) + + if (projectTotal !== expectedTotalFee) { + hasError = true + } + // console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0)) + if (hasError) { + setError("milestones", {message: "milestones is not valid", type: "invalid"}) + } else { + clearErrors("milestones") + } + }, [milestones]) + return ( <> diff --git a/src/components/CreateProject/MilestoneSection.tsx b/src/components/CreateProject/MilestoneSection.tsx index 9984875..5d66480 100644 --- a/src/components/CreateProject/MilestoneSection.tsx +++ b/src/components/CreateProject/MilestoneSection.tsx @@ -26,7 +26,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs from "dayjs"; import "dayjs/locale/zh-hk"; import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { useFormContext } from "react-hook-form"; +import { Controller, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import StyledDataGrid from "../StyledDataGrid"; import { INPUT_DATE_FORMAT, moneyFormatter } from "@/app/utils/formatUtil"; @@ -57,7 +57,9 @@ const MilestoneSection: React.FC = ({ taskGroupId }) => { const apiRef = useGridApiRef(); const addRow = useCallback(() => { - const id = Date.now(); + // const id = Date.now(); + const minId = Math.min(...payments.map((payment) => payment.id!!)); + const id = minId >= 0 ? -1 : minId - 1 setPayments((p) => [...p, { id, _isNew: true }]); setRowModesModel((model) => ({ ...model, @@ -239,21 +241,26 @@ const MilestoneSection: React.FC = ({ taskGroupId }) => { - { - if (!date) return; - const milestones = getValues("milestones"); - setValue("milestones", { - ...milestones, - [taskGroupId]: { - ...milestones[taskGroupId], - startDate: date.format(INPUT_DATE_FORMAT), - }, - }); - }} - /> + { + if (!date) return; + const milestones = getValues("milestones"); + setValue("milestones", { + ...milestones, + [taskGroupId]: { + ...milestones[taskGroupId], + startDate: date.format(INPUT_DATE_FORMAT), + }, + }); + }} + slotProps={{ + textField: { + error: startDate === "Invalid Date" || new Date(startDate) > new Date(endDate) || (Boolean(formState.errors.milestones) && !Boolean(startDate)), + }, + }} + /> @@ -272,6 +279,11 @@ const MilestoneSection: React.FC = ({ taskGroupId }) => { }, }); }} + slotProps={{ + textField: { + error: endDate === "Invalid Date" || new Date(startDate) > new Date(endDate) || (Boolean(formState.errors.milestones) && !Boolean(endDate)), + }, + }} /> diff --git a/src/components/CreateProject/ProjectTotalFee.tsx b/src/components/CreateProject/ProjectTotalFee.tsx index 247ed2d..edc7a71 100644 --- a/src/components/CreateProject/ProjectTotalFee.tsx +++ b/src/components/CreateProject/ProjectTotalFee.tsx @@ -2,7 +2,7 @@ import { CreateProjectInputs } from "@/app/api/projects/actions"; import { TaskGroup } from "@/app/api/tasks"; import { moneyFormatter } from "@/app/utils/formatUtil"; import { Divider, Stack, Typography } from "@mui/material"; -import React, { useCallback, useEffect, useMemo } from "react"; +import React from "react"; import { useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -12,21 +12,12 @@ interface Props { const ProjectTotalFee: React.FC = ({ taskGroups }) => { const { t } = useTranslation(); - const { watch, setError, clearErrors } = useFormContext(); + const { watch } = useFormContext(); const milestones = watch("milestones"); const expectedTotalFee = watch("expectedProjectFee"); let projectTotal = 0; - useEffect(() => { - console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseInt(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0)) - if (Object.keys(milestones).reduce((acc, key) => acc + milestones[parseInt(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0) !== expectedTotalFee) { - setError("milestones", {message: "project total is not valid", type: "invalid"}) - } else { - clearErrors("milestones") - } - }, [milestones]) - return ( {taskGroups.map((group, index) => { diff --git a/src/components/CreateProject/ResourceAllocation.tsx b/src/components/CreateProject/ResourceAllocation.tsx index c1e2706..b1a79b9 100644 --- a/src/components/CreateProject/ResourceAllocation.tsx +++ b/src/components/CreateProject/ResourceAllocation.tsx @@ -62,10 +62,10 @@ const ResourceAllocationByGrade: React.FC = ({ grades }) => { const manhourPercentageByGrade = watch("manhourPercentageByGrade"); const totalManhour = watch("totalManhour"); - const totalPercentage = Object.values(manhourPercentageByGrade).reduce( + const totalPercentage = Math.round(Object.values(manhourPercentageByGrade).reduce( (acc, percent) => acc + percent, 0, - ); + ) * 100) / 100; const makeUpdatePercentage = useCallback( (gradeId: Grade["id"]) => (percentage?: number) => { @@ -78,7 +78,7 @@ const ResourceAllocationByGrade: React.FC = ({ grades }) => { const keys = Object.keys(updatedManhourPercentageByGrade) if (keys.filter(k => updatedManhourPercentageByGrade[k as any] < 0).length > 0 || - keys.reduce((acc, value) => acc + updatedManhourPercentageByGrade[value as any], 0) !== 1) { + keys.reduce((acc, value) => acc + updatedManhourPercentageByGrade[value as any], 0) !== 100) { setError("manhourPercentageByGrade", {message: "manhourPercentageByGrade value is not valid", type: "invalid"}) } else { clearErrors("manhourPercentageByGrade") @@ -134,7 +134,8 @@ const ResourceAllocationByGrade: React.FC = ({ grades }) => { percentFormatter.format(val)} + renderValue={(val) => val + "%"} + // renderValue={(val) => percentFormatter.format(val)} onChange={makeUpdatePercentage(column.id)} convertValue={(inputValue) => Number(inputValue)} cellSx={{ backgroundColor: "primary.lightest" }} @@ -142,8 +143,9 @@ const ResourceAllocationByGrade: React.FC = ({ grades }) => { error={manhourPercentageByGrade[column.id] < 0} /> ))} - - {percentFormatter.format(totalPercentage)} + + {totalPercentage + "%"} + {/* {percentFormatter.format(totalPercentage)} */} @@ -151,7 +153,7 @@ const ResourceAllocationByGrade: React.FC = ({ grades }) => { {grades.map((column, idx) => ( {manhourFormatter.format( - manhourPercentageByGrade[column.id] * totalManhour, + manhourPercentageByGrade[column.id] / 100 * totalManhour, )} ))} @@ -203,7 +205,7 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { const keys = Object.keys(updatedTaskGroups) if (keys.filter(k => updatedTaskGroups[k as any].percentAllocation < 0).length > 0 || - keys.reduce((acc, value) => acc + updatedTaskGroups[value as any].percentAllocation, 0) !== 1) { + keys.reduce((acc, value) => acc + updatedTaskGroups[value as any].percentAllocation, 0) !== 100) { setError("taskGroups", {message: "Task Groups value is not invalid", type: "invalid"}) } else { clearErrors("taskGroups") @@ -250,7 +252,8 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { percentFormatter.format(val)} + // renderValue={(val) => percentFormatter.format(val)} + renderValue={(val) => val + "%"} onChange={makeUpdatePercentage(tg.id)} convertValue={(inputValue) => Number(inputValue)} cellSx={{ @@ -261,16 +264,16 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { /> {manhourFormatter.format( - currentTaskGroups[tg.id].percentAllocation * totalManhour, + currentTaskGroups[tg.id].percentAllocation / 100 * totalManhour, )} {grades.map((column, idx) => { const stageHours = - currentTaskGroups[tg.id].percentAllocation * totalManhour; + currentTaskGroups[tg.id].percentAllocation / 100 * totalManhour; return ( {manhourFormatter.format( - manhourPercentageByGrade[column.id] * stageHours, + manhourPercentageByGrade[column.id] / 100 * stageHours, )} ); @@ -286,13 +289,13 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { )} acc + tg.percentAllocation, 0,) === 1 && leftBorderCellSx), - ...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) !== 1 && errorCellSx) + ...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) === 100 && leftBorderCellSx), + ...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) !== 100 && errorCellSx) }} > {percentFormatter.format( Object.values(currentTaskGroups).reduce( - (acc, tg) => acc + tg.percentAllocation, + (acc, tg) => acc + tg.percentAllocation / 100, 0, ), )} @@ -300,7 +303,7 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { {manhourFormatter.format( Object.values(currentTaskGroups).reduce( - (acc, tg) => acc + tg.percentAllocation * totalManhour, + (acc, tg) => acc + tg.percentAllocation / 100 * totalManhour, 0, ), )} @@ -309,9 +312,9 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { const hours = Object.values(currentTaskGroups).reduce( (acc, tg) => acc + - tg.percentAllocation * + tg.percentAllocation / 100 * totalManhour * - manhourPercentageByGrade[column.id], + manhourPercentageByGrade[column.id] / 100 , 0, ); return ( diff --git a/src/components/CreateProject/TaskSetup.tsx b/src/components/CreateProject/TaskSetup.tsx index 443014e..6651971 100644 --- a/src/components/CreateProject/TaskSetup.tsx +++ b/src/components/CreateProject/TaskSetup.tsx @@ -52,7 +52,7 @@ const TaskSetup: React.FC = ({ (e: SelectChangeEvent) => { if (e.target.value === "All" || isNumber(e.target.value)) { setSelectedTaskTemplateId(e.target.value); - onReset(); + // onReset(); } }, [onReset], diff --git a/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx b/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx index f7d5912..856f66e 100644 --- a/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx +++ b/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx @@ -13,7 +13,7 @@ import Close from "@mui/icons-material/Close"; import { useRouter, useSearchParams } from "next/navigation"; import React from "react"; import Stack from "@mui/material/Stack"; -import { Task } from "@/app/api/tasks"; +import { Task, TaskTemplate } from "@/app/api/tasks"; import { NewTaskTemplateFormInputs, fetchTaskTemplate, @@ -24,9 +24,10 @@ import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts"; interface Props { tasks: Task[]; + defaultInputs?: TaskTemplate; } -const CreateTaskTemplate: React.FC = ({ tasks }) => { +const CreateTaskTemplate: React.FC = ({ tasks, defaultInputs }) => { const { t } = useTranslation(); const searchParams = useSearchParams() @@ -54,45 +55,45 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { watch, resetField, formState: { errors, isSubmitting }, - } = useForm({ defaultValues: { taskIds: [] } }); + } = useForm({ defaultValues: defaultInputs }); const currentTaskIds = watch("taskIds"); const selectedItems = React.useMemo(() => { return items.filter((item) => currentTaskIds.includes(item.id)); }, [currentTaskIds, items]); - const [refTaskTemplate, setRefTaskTemplate] = React.useState() - const id = searchParams.get('id') - - const fetchCurrentTaskTemplate = async () => { - try { - const taskTemplate = await fetchTaskTemplate(parseInt(id!!)) - - const defaultValues = { - id: parseInt(id!!), - code: taskTemplate.code ?? null, - name: taskTemplate.name ?? null, - taskIds: taskTemplate.tasks.map(task => task.id) ?? [], - } - - setRefTaskTemplate(defaultValues) - } catch (e) { - console.log(e) - } - } - - React.useLayoutEffect(() => { - if (id !== null && parseInt(id) > 0) fetchCurrentTaskTemplate() - }, [id]) - - React.useEffect(() => { - if (refTaskTemplate) { - setValue("taskIds", refTaskTemplate.taskIds) - resetField("code", { defaultValue: refTaskTemplate.code }) - resetField("name", { defaultValue: refTaskTemplate.name }) - setValue("id", refTaskTemplate.id) - } - }, [refTaskTemplate]) + // const [refTaskTemplate, setRefTaskTemplate] = React.useState() + // const id = searchParams.get('id') + + // const fetchCurrentTaskTemplate = async () => { + // try { + // const taskTemplate = await fetchTaskTemplate(parseInt(id!!)) + + // const defaultValues = { + // id: parseInt(id!!), + // code: taskTemplate.code ?? null, + // name: taskTemplate.name ?? null, + // taskIds: taskTemplate.tasks.map(task => task.id) ?? [], + // } + + // setRefTaskTemplate(defaultValues) + // } catch (e) { + // console.log(e) + // } + // } + + // React.useLayoutEffect(() => { + // if (id !== null && parseInt(id) > 0) fetchCurrentTaskTemplate() + // }, [id]) + + // React.useEffect(() => { + // if (refTaskTemplate) { + // setValue("taskIds", refTaskTemplate.taskIds) + // resetField("code", { defaultValue: refTaskTemplate.code }) + // resetField("name", { defaultValue: refTaskTemplate.name }) + // setValue("id", refTaskTemplate.id) + // } + // }, [refTaskTemplate]) const onSubmit: SubmitHandler = React.useCallback( async (data) => { @@ -120,9 +121,9 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { return ( <> - { - (id === null || refTaskTemplate !== undefined) && + + {/* Task List Setup */} {t("Task List Setup")} = ({ tasks }) => { allItemsLabel={t("Task Pool")} selectedItemsLabel={t("Task List Template")} /> + {/* Task List Setup */} + {/* Task List Setup */} + { @@ -188,7 +192,7 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { {t("Confirm")} - } + ); }; diff --git a/src/components/CreateTaskTemplate/CreateTaskTemplateWrapper.tsx b/src/components/CreateTaskTemplate/CreateTaskTemplateWrapper.tsx index 77888a2..50a131b 100644 --- a/src/components/CreateTaskTemplate/CreateTaskTemplateWrapper.tsx +++ b/src/components/CreateTaskTemplate/CreateTaskTemplateWrapper.tsx @@ -1,11 +1,18 @@ import React from "react"; import CreateTaskTemplate from "./CreateTaskTemplate"; -import { fetchAllTasks } from "@/app/api/tasks"; +import { fetchAllTasks, fetchTaskTemplate } from "@/app/api/tasks"; -const CreateTaskTemplateWrapper: React.FC = async () => { - const tasks = await fetchAllTasks(); +interface Props { + taskTemplateId?: string; +} - return ; +const CreateTaskTemplateWrapper: React.FC = async (props) => { + const [tasks] = await Promise.all([ + fetchAllTasks(), + ]); + + const taskTemplateInfo = props.taskTemplateId ? await fetchTaskTemplate(props.taskTemplateId) : undefined + return ; }; export default CreateTaskTemplateWrapper; diff --git a/src/components/EditSkill/EditSkill.tsx b/src/components/EditSkill/EditSkill.tsx new file mode 100644 index 0000000..a34b6e9 --- /dev/null +++ b/src/components/EditSkill/EditSkill.tsx @@ -0,0 +1,151 @@ +"use client"; +import { SkillResult } from "@/app/api/skill"; +import { + Button, + Card, + CardContent, + Grid, + Stack, + Tab, + Tabs, + TabsProps, + TextField, + Typography, +} from "@mui/material"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, + useFormContext, +} from "react-hook-form"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; +import EditSkillForm from "./EditSkillForm"; +import { CreateSkillInputs, saveSkill } from "@/app/api/skill/actions"; +import AuthAllocation from "../EditUser/AuthAllocation"; + +interface Props { + skills: SkillResult[]; +} + +const EditSkill: React.FC = async ({ skills }) => { + const { t } = useTranslation(); + const formProps = useForm(); + const [serverError, setServerError] = useState(""); + const router = useRouter(); + const searchParams = useSearchParams(); + const id = parseInt(searchParams.get("id") || "0"); + const [tabIndex, setTabIndex] = useState(0); + const [filteredSkill, setFilteredSkill] = useState(() => + skills.filter((s) => s.id === id)[0] as SkillResult + ); + const errors = formProps.formState.errors; + + const onSubmit = useCallback>( + async (data) => { + try { + console.log(data); + const postData = { + ...data, + id: id + } + await saveSkill(postData) + router.replace(`/settings/skill`) + } catch (e) { + console.log(e); + setServerError(t("An error has occurred. Please try again later.")); + } + }, + [router] + ); + + const handleCancel = () => { + router.back(); + }; + + const handleTabChange = useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [] + ); + + useEffect(() => { + formProps.reset({ + name: filteredSkill.name, + code: filteredSkill.code, + description: filteredSkill.description + }); + }, [skills]); + + const hasErrorsInTab = ( + tabIndex: number, + errors: FieldErrors + ) => { + switch (tabIndex) { + case 0: + return Object.keys(errors).length > 0; + default: + false; + } + }; + + return ( + <> + {serverError && ( + + {serverError} + + )} + + + + + ) : undefined + } + iconPosition="end" + /> + {/* */} + + {tabIndex === 0 && } + + + + + + + + + ); +}; +export default EditSkill; diff --git a/src/components/EditSkill/EditSkillForm.tsx b/src/components/EditSkill/EditSkillForm.tsx new file mode 100644 index 0000000..120d2e5 --- /dev/null +++ b/src/components/EditSkill/EditSkillForm.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { CreateSkillInputs } from "@/app/api/skill/actions"; +import { + Box, + Button, + Card, + CardContent, + Grid, + Stack, + Tab, + Tabs, + TabsProps, + TextField, + Typography, +} from "@mui/material"; +import { useSearchParams } from "next/navigation"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, + useFormContext, +} from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +interface Props { + // users: UserResult[] +} + +const EditSkillForm: React.FC = async ({}) => { + const { t } = useTranslation(); + const searchParams = useSearchParams(); + const idString = searchParams.get("id"); + const { + register, + setValue, + getValues, + formState: { errors, defaultValues }, + reset, + resetField, + } = useFormContext(); + // const formProps = useForm({}); + + return ( + <> + + + + + {t("Skill Info")} + + + + + + + + + + + + + + + + + ); +}; +export default EditSkillForm; diff --git a/src/components/EditSkill/EditSkillLoading.tsx b/src/components/EditSkill/EditSkillLoading.tsx new file mode 100644 index 0000000..74e08af --- /dev/null +++ b/src/components/EditSkill/EditSkillLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const EditSkillLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + Edit Skill + + + + + + + + + + + ); +}; + +export default EditSkillLoading; diff --git a/src/components/EditSkill/EditSkillWrapper.tsx b/src/components/EditSkill/EditSkillWrapper.tsx new file mode 100644 index 0000000..12d7a12 --- /dev/null +++ b/src/components/EditSkill/EditSkillWrapper.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import EditSkill from "./EditSkill"; +import EditSkillLoading from "./EditSkillLoading"; +import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; +import { useSearchParams } from "next/navigation"; +import { fetchSkill } from "@/app/api/skill"; + +interface SubComponents { + Loading: typeof EditSkillLoading; +} + +const EditSkillWrapper: React.FC & SubComponents = async () => { + const skills = await fetchSkill() + console.log(skills) + + return ; +}; + +EditSkillWrapper.Loading = EditSkillLoading; + +export default EditSkillWrapper; diff --git a/src/components/EditSkill/index.ts b/src/components/EditSkill/index.ts new file mode 100644 index 0000000..ba42dd8 --- /dev/null +++ b/src/components/EditSkill/index.ts @@ -0,0 +1 @@ +export { default } from "./EditSkillWrapper"; diff --git a/src/components/EditTeam/Allocation.tsx b/src/components/EditTeam/Allocation.tsx index 61e9e8f..f1386fe 100644 --- a/src/components/EditTeam/Allocation.tsx +++ b/src/components/EditTeam/Allocation.tsx @@ -16,8 +16,8 @@ import { Staff4TransferList, fetchStaffCombo } from "@/app/api/staff/actions"; import { StaffResult, StaffTeamTable } from "@/app/api/staff"; import SearchResults, { Column } from "../SearchResults"; import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material"; -import { Card } from "reactstrap"; import { + Card, Box, CardContent, Grid, @@ -49,8 +49,6 @@ const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { reset, resetField, } = useFormContext(); - - // let firstFilter: StaffResult[] = [] const initialStaffs = staff.map((s) => ({ ...s })); const [filteredStaff, setFilteredStaff] = useState(initialStaffs); diff --git a/src/components/EditUser/AuthAllocation.tsx b/src/components/EditUser/AuthAllocation.tsx new file mode 100644 index 0000000..afb44d5 --- /dev/null +++ b/src/components/EditUser/AuthAllocation.tsx @@ -0,0 +1,221 @@ +"use client"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Add, Clear, PersonAdd, PersonRemove, Remove, Search } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, + useFormContext, + } from "react-hook-form"; +import { + Box, + Card, + CardContent, + Grid, + IconButton, + InputAdornment, + Stack, + Tab, + Tabs, + TabsProps, + TextField, + Typography, + } from "@mui/material"; + import { differenceBy } from "lodash"; +import { UserInputs } from "@/app/api/user/actions"; +import { auth } from "@/app/api/group/actions"; +import SearchResults, { Column } from "../SearchResults"; + +export interface Props { + auths: auth[] + + } + +const AuthAllocation: React.FC = ({ auths }) => { + const { t } = useTranslation(); + const searchParams = useSearchParams(); + const id = parseInt(searchParams.get("id") || "0"); + const { + setValue, + getValues, + formState: { defaultValues }, + reset, + resetField, + } = useFormContext(); + const initialAuths = auths.map((u) => ({ ...u })).sort((a, b) => a.id - b.id); + const [filteredAuths, setFilteredAuths] = useState(initialAuths); + const [selectedAuths, setSelectedAuths] = useState( + () => { + return filteredAuths.filter( + (s) => getValues("addAuthIds")?.includes(s.id) + ); + } + ); + const [removeAuthIds, setRemoveAuthIds] = useState([]); + + // Adding / Removing Auth + const addAuth = useCallback((auth: auth) => { + setSelectedAuths((a) => [...a, auth]); + }, []); + const removeAuth = useCallback((auth: auth) => { + setSelectedAuths((a) => a.filter((a) => a.id !== auth.id)); + setRemoveAuthIds((prevIds) => [...prevIds, auth.id]); + }, []); + + const clearAuth = useCallback(() => { + if (defaultValues !== undefined) { + resetField("addAuthIds"); + setSelectedAuths( + initialAuths.filter((auth) => defaultValues.addAuthIds?.includes(auth.id)) + ); + } + }, [defaultValues]); + + // Sync with form + useEffect(() => { + setValue( + "addAuthIds", + selectedAuths.map((a) => a.id) + ); + setValue( + "removeAuthIds", + removeAuthIds + ); + }, [selectedAuths, removeAuthIds, setValue]); + + + const AuthPoolColumns = useMemo[]>( + () => [ + { + label: t("Add"), + name: "id", + onClick: addAuth, + buttonIcon: , + }, + { label: t("authority"), name: "authority" }, + { label: t("Auth Name"), name: "name" }, + // { label: t("Current Position"), name: "currentPosition" }, + ], + [addAuth, t] + ); + + const allocatedAuthColumns = useMemo[]>( + () => [ + { + label: t("Remove"), + name: "id", + onClick: removeAuth, + buttonIcon: , + }, + { label: t("authority"), name: "authority" }, + { label: t("Auth Name"), name: "name" }, + ], + [removeAuth, selectedAuths, t] + ); + + const [query, setQuery] = React.useState(""); + const onQueryInputChange = React.useCallback< + React.ChangeEventHandler + >((e) => { + setQuery(e.target.value); + }, []); + const clearQueryInput = React.useCallback(() => { + setQuery(""); + }, []); + + React.useEffect(() => { + // setFilteredStaff( + // initialStaffs.filter((s) => { + // const q = query.toLowerCase(); + // // s.staffId.toLowerCase().includes(q) + // // const q = query.toLowerCase(); + // // return s.name.toLowerCase().includes(q); + // // s.code.toString().includes(q) || + // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) + // }) + // ); + }, [auths, query]); + + const resetAuth = React.useCallback(() => { + clearQueryInput(); + clearAuth(); + }, [clearQueryInput, clearAuth]); + + const formProps = useForm({}); + + // Tab related + const [tabIndex, setTabIndex] = React.useState(0); + const handleTabChange = React.useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [] + ); + +return ( + <> + + + + + + {t("Authority")} + + + + + + + + + + ), + }} + /> + + + + + + + + {tabIndex === 0 && ( + + )} + {tabIndex === 1 && ( + + )} + + + + + + + ); + +} +export default AuthAllocation \ No newline at end of file diff --git a/src/components/EditUser/EditUser.tsx b/src/components/EditUser/EditUser.tsx index db52fac..853de6a 100644 --- a/src/components/EditUser/EditUser.tsx +++ b/src/components/EditUser/EditUser.tsx @@ -1,6 +1,6 @@ "use client"; import { useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react"; import SearchResults, { Column } from "../SearchResults"; // import { TeamResult } from "@/app/api/team"; import { useTranslation } from "react-i18next"; @@ -26,9 +26,11 @@ import { } from "react-hook-form"; import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; import { StaffResult } from "@/app/api/staff"; -import { editUser, fetchUserDetails } from "@/app/api/user/actions"; +import { UserInputs, editUser, fetchUserDetails } from "@/app/api/user/actions"; import UserDetail from "./UserDetail"; import { UserResult } from "@/app/api/user"; +import { auth, fetchAuth } from "@/app/api/group/actions"; +import AuthAllocation from "./AuthAllocation"; interface Props { // users: UserResult[] @@ -36,11 +38,14 @@ interface Props { const EditUser: React.FC = async ({ }) => { const { t } = useTranslation(); - const formProps = useForm(); + const formProps = useForm(); const searchParams = useSearchParams(); const id = parseInt(searchParams.get("id") || "0"); const [tabIndex, setTabIndex] = useState(0); const router = useRouter(); + const [serverError, setServerError] = useState(""); + const [data, setData] = useState(); + const [auths, setAuths] = useState(); const handleTabChange = useCallback>( (_e, newValue) => { @@ -49,38 +54,45 @@ const EditUser: React.FC = async ({ }) => { [] ); - const [serverError, setServerError] = useState(""); - const [data, setData] = useState(); + const errors = formProps.formState.errors; const fetchUserDetail = async () => { console.log(id); try { + // fetch user info const userDetail = await fetchUserDetails(id); console.log(userDetail); const _data = userDetail.data as UserResult; console.log(_data); setData(_data); + //fetch user auths + const authDetail = await fetchAuth("user", id); + setAuths(authDetail.records) + const addAuthIds = authDetail.records.filter((item) => item.v === 1).map((item) => item.id).sort((a, b) => a - b); + formProps.reset({ - username: _data.username, - firstname: _data.firstname, - lastname: _data.lastname, - title: _data.title, - department: _data.department, + name: _data.username, email: _data.email, - phone1: _data.phone1, - phone2: _data.phone2, - remarks: _data.remarks, + addAuthIds: addAuthIds || [] }); } catch (error) { console.log(error); setServerError(t("An error has occurred. Please try again later.")); } - }; + } useEffect(() => { fetchUserDetail(); }, []); + // useEffect(() => { + // const thisUser = users.filter((item) => item.id === id) + // formProps.reset({ + // username: thisUser[0].username, + // email: thisUser[0].email, + // }); + // }, []); + const hasErrorsInTab = ( tabIndex: number, errors: FieldErrors @@ -97,14 +109,16 @@ const EditUser: React.FC = async ({ }) => { router.back(); }; - const onSubmit = useCallback>( + const onSubmit = useCallback>( async (data) => { try { console.log(data); const tempData = { - username: data.username, + name: data.name, email: data.email, - locked: false + locked: false, + addAuthIds: data.addAuthIds || [], + removeAuthIds: data.removeAuthIds || [], } console.log(tempData); await editUser(id, tempData); @@ -116,7 +130,7 @@ const EditUser: React.FC = async ({ }) => { }, [router] ); - const onSubmitError = useCallback>( + const onSubmitError = useCallback>( (errors) => { console.log(errors); }, @@ -136,7 +150,31 @@ const EditUser: React.FC = async ({ }) => { component="form" onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} > - + + + + ) : undefined + } + iconPosition="end" + /> + + + + {tabIndex == 0 && } + {tabIndex === 1 && } + +
+ +
+ + +
{ color?: IconButtonOwnProps["color"]; needTranslation?: boolean; type?: string; + isHidden?: boolean; } interface ColumnWithAction extends BaseColumn { @@ -78,7 +79,7 @@ function SearchResults({ - {columns.map((column, idx) => ( + {columns.filter(item => item.isHidden !== true).map((column, idx) => ( {column?.type === "money" ?
{column.label}
: column.label}
@@ -91,7 +92,7 @@ function SearchResults({ .map((item) => { return ( - {columns.map((column, idx) => { + {columns.filter(item => item.isHidden !== true).map((column, idx) => { const columnName = column.name; return ( diff --git a/src/components/SkillSearch/SkillSearch.tsx b/src/components/SkillSearch/SkillSearch.tsx index 01db336..c13ea19 100644 --- a/src/components/SkillSearch/SkillSearch.tsx +++ b/src/components/SkillSearch/SkillSearch.tsx @@ -18,16 +18,21 @@ type SearchParamNames = keyof SearchQuery; const SkillSearch: React.FC = ({ skill }) => { const { t } = useTranslation(); - const [filteredStaff, setFilteredStaff] = useState(skill); + const [filteredSkill, setFilteredSkill] = useState(skill); const router = useRouter(); const searchCriteria: Criterion[] = useMemo( () => [ { - label: t("Staff Name"), + label: t("Skill Name"), paramName: "name", type: "text", }, + { + label: t("Skill code"), + paramName: "code", + type: "text", + }, ], [t] ); @@ -36,7 +41,7 @@ const SkillSearch: React.FC = ({ skill }) => { (skill: SkillResult) => { console.log(skill); const id = skill.id; - // router.push(`/settings/skill/edit?id=${id}`); + router.push(`/settings/skill/edit?id=${id}`); }, [router, t] ); @@ -45,7 +50,7 @@ const SkillSearch: React.FC = ({ skill }) => { // deleteDialog(async () => { // await deleteStaff(skill.id); // successDialog("Delete Success", t); - // setFilteredStaff((prev) => prev.filter((obj) => obj.id !== skill.id)); + // setFilteredSkill((prev) => prev.filter((obj) => obj.id !== skill.id)); // }, t); }, []); @@ -76,19 +81,16 @@ const SkillSearch: React.FC = ({ skill }) => { { - // setFilteredStaff( - // skill.filter( - // (s) => - // s.skillId.toLowerCase().includes(query.skillId.toLowerCase()) && - // s.name.toLowerCase().includes(query.name.toLowerCase()) - // // (query.team === "All" || s.team === query.team) && - // // (query.category === "All" || s.category === query.category) && - // // (query.team === "All" || s.team === query.team), - // ) - // ); + setFilteredSkill( + skill.filter( + (s) => + s.name.toLowerCase().includes(query.name.toLowerCase()) && + s.code.toLowerCase().includes(query.code.toLowerCase()) + ) + ); }} /> - items={filteredStaff} columns={columns} /> + items={filteredSkill} columns={columns} /> ); }; diff --git a/src/components/StaffSearch/StaffSearch.tsx b/src/components/StaffSearch/StaffSearch.tsx index 5bfb017..e19e915 100644 --- a/src/components/StaffSearch/StaffSearch.tsx +++ b/src/components/StaffSearch/StaffSearch.tsx @@ -10,15 +10,17 @@ import { deleteStaff } from "@/app/api/staff/actions"; import { useRouter } from "next/navigation"; import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; import Person from '@mui/icons-material/Person'; +import { MAINTAIN_USER, VIEW_USER } from "@/middleware"; interface Props { staff: StaffResult[]; + abilities: string[] } type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; -const StaffSearch: React.FC = ({ staff }) => { +const StaffSearch: React.FC = ({ staff, abilities }) => { const { t } = useTranslation(); const [filteredStaff, setFilteredStaff] = useState(staff); const router = useRouter(); @@ -95,6 +97,7 @@ const StaffSearch: React.FC = ({ staff }) => { label: t("Actions"), onClick: onUserClick, buttonIcon: , + isHidden: ![MAINTAIN_USER, VIEW_USER].some((ability) => abilities.includes(ability)), }, { name: "team", label: t("Team") }, { name: "name", label: t("Staff Name") }, diff --git a/src/components/StaffSearch/StaffSearchWrapper.tsx b/src/components/StaffSearch/StaffSearchWrapper.tsx index 59f9dc1..4f3b57d 100644 --- a/src/components/StaffSearch/StaffSearchWrapper.tsx +++ b/src/components/StaffSearch/StaffSearchWrapper.tsx @@ -2,16 +2,8 @@ import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; import React from "react"; import StaffSearch from "./StaffSearch"; import StaffSearchLoading from "./StaffSearchLoading"; -import { comboProp, fetchCompanyCombo } from "@/app/api/companys/actions"; -import { fetchTeamCombo } from "@/app/api/team/actions"; -import { fetchDepartmentCombo } from "@/app/api/departments/actions"; -import { fetchPositionCombo } from "@/app/api/positions/actions"; -import { fetchGradeCombo } from "@/app/api/grades/actions"; -import { fetchSkillCombo } from "@/app/api/skill/actions"; -import { fetchSalaryCombo } from "@/app/api/salarys/actions"; import { Session, getServerSession } from "next-auth"; import { authOptions } from "@/config/authConfig"; -// import { preloadStaff } from "@/app/api/staff"; interface SubComponents { Loading: typeof StaffSearchLoading; @@ -24,9 +16,9 @@ interface SessionWithAbilities extends Session { const StaffSearchWrapper: React.FC & SubComponents = async () => { const staff = await fetchStaff(); const session = await getServerSession(authOptions) as SessionWithAbilities; - console.log(session.abilities); + const abilities: string[] = session.abilities!! - return ; + return ; }; StaffSearchWrapper.Loading = StaffSearchLoading; diff --git a/src/components/StaffUtilization/StaffUtilization.tsx b/src/components/StaffUtilization/StaffUtilization.tsx index 81dde4b..74dc3eb 100644 --- a/src/components/StaffUtilization/StaffUtilization.tsx +++ b/src/components/StaffUtilization/StaffUtilization.tsx @@ -124,6 +124,8 @@ const StaffUtilization: React.FC = () => { const [manHoursSpentPeriod, setManHoursSpentPeriod]: any[] = React.useState( firstDayOfWeekString + " to " + lastDayOfWeekString, ); + const [unsubmittedTimeSheetSelect, setUnsubmittedTimeSheetSelect]: any = + React.useState("Weekly"); const [teamTotalManhoursSpentSelect, setTeamTotalManhoursSpentSelect]: any = React.useState("Weekly"); const [staffGradeManhoursSpentSelect, setStaffGradeManhoursSpentSelect]: any = @@ -165,6 +167,10 @@ const StaffUtilization: React.FC = () => { individualStaffManhoursSpentPeriod, setIndividualStaffManhoursSpentPeriod, ]: any[] = React.useState(weekDates); + const [ + unsubmittedTimeSheetPeriod, + setUnsubmittedTimeSheetPeriod, + ]: any[] = React.useState(weekDates); const [ teamTotalManhoursSpentPlanData, setTeamTotalManhoursSpentPlanData, @@ -179,6 +185,8 @@ const StaffUtilization: React.FC = () => { React.useState(dayjs()); const [weeklyValueByIndividualStaff, setWeeklyValueByIndividualStaff] = React.useState(dayjs()); + const [weeklyUnsubmittedTimeSheet, setWeeklyUnsubmittedTimeSheet ] = + React.useState(dayjs()); const [staffGradeManhoursSpentValue, setStaffGradeManhoursSpentValue] = React.useState(dayjs()); const [totalManHoursMonthlyFromValue, setTotalManHoursMonthlyFromValue] = @@ -424,6 +432,56 @@ const StaffUtilization: React.FC = () => { ], }; + const unsubmittedTimeSheetOptions: ApexOptions = { + chart: { + height: 350, + type: "line", + }, + stroke: { + width: [1], + }, + plotOptions: { + bar: { + horizontal: true, + distributed: false, + }, + }, + dataLabels: { + enabled: true, + }, + xaxis: { + categories: [ + "001-Staff A", + "002-Staff B", + "005-Staff E", + "006-Staff F", + "007-Staff G", + ], + }, + yaxis: [ + { + title: { + text: "Staff", + }, + min: 0, + max: 12, + tickAmount: 5, + }, + ], + grid: { + borderColor: "#f1f1f1", + }, + annotations: {}, + series: [ + { + name: "Unsubmitted Time Sheet", + type: "bar", + color: "#00acb1", + data: [2, 2, 1, 5, 1], + }, + ], + }; + const teamTotalManhoursSpentOnClick = (r: any) => { setTeamTotalManhoursSpentSelect(r); if (r === "Weekly") { @@ -456,6 +514,10 @@ const StaffUtilization: React.FC = () => { // } }; + const unsubmittedTimeSheetOnClick = (r: any) => { + setUnsubmittedTimeSheetSelect(r); + }; + const selectWeeklyPeriod = (r: any) => { const selectDate = new Date(r); const firstDayOfWeek = new Date(); @@ -486,6 +548,21 @@ const StaffUtilization: React.FC = () => { setWeeklyValueByStaffGrade(dayjs(firstDayOfWeek)); }; + const selectWeeklyPeriodUnsubmittedTimeSheet = (r: any) => { + const selectDate = new Date(r); + const firstDayOfWeek = new Date(); + firstDayOfWeek.setDate(selectDate.getDate() - selectDate.getDay() + 0); + const weekDates: any[] = []; + for (let i = 0; i < 7; i++) { + const currentDate = new Date(firstDayOfWeek); + currentDate.setDate(firstDayOfWeek.getDate() + i); + const formattedDate = dayjs(currentDate).format("DD MMM (ddd)"); + weekDates.push(formattedDate); + } + setUnsubmittedTimeSheetPeriod(weekDates); + setWeeklyUnsubmittedTimeSheet(dayjs(firstDayOfWeek)); + }; + const selectWeeklyPeriodIndividualStaff = (r: any) => { const selectDate = new Date(r); const firstDayOfWeek = new Date(); @@ -589,6 +666,28 @@ const StaffUtilization: React.FC = () => { setTeamTotalManhoursByStaffGrade(weekDates); }; + const selectUnsubmittedTimeSheetMonthlyPeriodFrom = (r: any) => { + const monthDates: any[] = []; + const monthPlanData: any[] = []; + const monthActualData: any[] = []; + const selectFromDate = dayjs(r); + for ( + let date = selectFromDate.clone(); + date.isBefore(totalManHoursMonthlyToValue, "month"); + date = date.add(1, "month") + ) { + monthDates.push(date.format("MM-YYYY")); + monthPlanData.push(840); + monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840)); + } + monthDates.push(totalManHoursMonthlyToValue.format("MM-YYYY")); + monthPlanData.push(840); + monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840)); + // setTeamTotalManhoursSpentPlanData(monthPlanData) + // setTeamTotalManhoursSpentActualData(monthActualData) + setUnsubmittedTimeSheetPeriod(weekDates); + }; + const selectIndividualStaffMonthlyPeriodFrom = (r: any) => { const monthDates: any[] = []; const monthPlanData: any[] = []; @@ -611,6 +710,28 @@ const StaffUtilization: React.FC = () => { setIndividualStaffManhoursSpentPeriod(weekDates); }; + const selectUnsubmittedTimeSheetMonthlyPeriodTo = (r: any) => { + const monthDates: any[] = []; + const monthPlanData: any[] = []; + const monthActualData: any[] = []; + const selectToDate = dayjs(r); + for ( + let date = totalManHoursMonthlyFromValue.clone(); + date.isBefore(selectToDate, "month"); + date = date.add(1, "month") + ) { + monthDates.push(date.format("MM-YYYY")); + monthPlanData.push(840); + monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840)); + } + monthDates.push(selectToDate.format("MM-YYYY")); + monthPlanData.push(840); + monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840)); + // setTeamTotalManhoursSpentPlanData(monthPlanData) + // setTeamTotalManhoursSpentActualData(monthActualData) + setUnsubmittedTimeSheetPeriod(weekDates); + }; + const selectIndividualStaffMonthlyPeriodTo = (r: any) => { const monthDates: any[] = []; const monthPlanData: any[] = []; @@ -934,7 +1055,125 @@ const StaffUtilization: React.FC = () => { }} > - + + +
+
+ {unsubmittedTimeSheetSelect === "Weekly" && ( + <> + + + + )} + {unsubmittedTimeSheetSelect === "Monthly" && ( + <> + + + + )} +
+
+
+ +
+
+