@@ -14,6 +14,7 @@ | |||||
"@faker-js/faker": "^8.4.1", | "@faker-js/faker": "^8.4.1", | ||||
"@fontsource/inter": "^5.0.16", | "@fontsource/inter": "^5.0.16", | ||||
"@fontsource/plus-jakarta-sans": "^5.0.18", | "@fontsource/plus-jakarta-sans": "^5.0.18", | ||||
"@fullcalendar/react": "^6.1.11", | |||||
"@mui/icons-material": "^5.15.0", | "@mui/icons-material": "^5.15.0", | ||||
"@mui/material": "^5.15.0", | "@mui/material": "^5.15.0", | ||||
"@mui/material-nextjs": "^5.15.0", | "@mui/material-nextjs": "^5.15.0", | ||||
@@ -21,13 +22,14 @@ | |||||
"@mui/x-date-pickers": "^6.18.7", | "@mui/x-date-pickers": "^6.18.7", | ||||
"@unly/universal-language-detector": "^2.0.3", | "@unly/universal-language-detector": "^2.0.3", | ||||
"apexcharts": "^3.45.2", | "apexcharts": "^3.45.2", | ||||
"axios": "^1.6.8", | |||||
"date-holidays": "^3.23.11", | |||||
"dayjs": "^1.11.10", | "dayjs": "^1.11.10", | ||||
"fullcalendar": "^6.1.11", | |||||
"i18next": "^23.7.11", | "i18next": "^23.7.11", | ||||
"i18next-resources-to-backend": "^1.2.0", | "i18next-resources-to-backend": "^1.2.0", | ||||
"lodash": "^4.17.21", | "lodash": "^4.17.21", | ||||
"next": "14.0.4", | "next": "14.0.4", | ||||
"next-auth": "^4.24.5", | |||||
"next-auth": "^4.24.7", | |||||
"next-pwa": "^5.6.0", | "next-pwa": "^5.6.0", | ||||
"react": "^18", | "react": "^18", | ||||
"react-apexcharts": "^1.4.1", | "react-apexcharts": "^1.4.1", | ||||
@@ -2081,6 +2083,79 @@ | |||||
"tslib": "^2.4.0" | "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": { | "node_modules/@humanwhocodes/config-array": { | ||||
"version": "0.11.14", | "version": "0.11.14", | ||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", | "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", | ||||
@@ -3648,8 +3723,7 @@ | |||||
"node_modules/argparse": { | "node_modules/argparse": { | ||||
"version": "2.0.1", | "version": "2.0.1", | ||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", | "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": { | "node_modules/aria-query": { | ||||
"version": "5.3.0", | "version": "5.3.0", | ||||
@@ -3855,6 +3929,14 @@ | |||||
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", | "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", | ||||
"dev": true | "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": { | "node_modules/async": { | ||||
"version": "3.2.5", | "version": "3.2.5", | ||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", | "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", | ||||
@@ -3869,11 +3951,6 @@ | |||||
"has-symbols": "^1.0.3" | "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": { | "node_modules/at-least-node": { | ||||
"version": "1.0.0", | "version": "1.0.0", | ||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", | "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", | ||||
@@ -3942,16 +4019,6 @@ | |||||
"node": ">=4" | "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": { | "node_modules/axobject-query": { | ||||
"version": "3.2.1", | "version": "3.2.1", | ||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", | "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", | ||||
@@ -4136,6 +4203,17 @@ | |||||
"node": ">=10.16.0" | "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": { | "node_modules/call-bind": { | ||||
"version": "1.0.7", | "version": "1.0.7", | ||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", | "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", | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", | ||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" | "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": { | "node_modules/commander": { | ||||
"version": "4.1.1", | "version": "4.1.1", | ||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", | "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", | ||||
@@ -4489,6 +4556,68 @@ | |||||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", | "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", | ||||
"dev": true | "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": { | "node_modules/dayjs": { | ||||
"version": "1.11.10", | "version": "1.11.10", | ||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", | "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", | ||||
@@ -4626,14 +4755,6 @@ | |||||
"rimraf": "bin.js" | "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": { | "node_modules/dequal": { | ||||
"version": "2.0.3", | "version": "2.0.3", | ||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", | "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", | ||||
@@ -5706,25 +5827,6 @@ | |||||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", | "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", | ||||
"dev": true | "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": { | "node_modules/for-each": { | ||||
"version": "0.3.3", | "version": "0.3.3", | ||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", | "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": "^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": { | "node_modules/function-bind": { | ||||
"version": "1.1.2", | "version": "1.1.2", | ||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", | ||||
@@ -6755,6 +6870,11 @@ | |||||
"node": ">=8" | "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": { | "node_modules/jest-worker": { | ||||
"version": "27.5.1", | "version": "27.5.1", | ||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", | ||||
@@ -6800,9 +6920,9 @@ | |||||
} | } | ||||
}, | }, | ||||
"node_modules/jose": { | "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": { | "funding": { | ||||
"url": "https://github.com/sponsors/panva" | "url": "https://github.com/sponsors/panva" | ||||
} | } | ||||
@@ -6816,7 +6936,6 @@ | |||||
"version": "4.1.0", | "version": "4.1.0", | ||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", | ||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", | ||||
"dev": true, | |||||
"dependencies": { | "dependencies": { | ||||
"argparse": "^2.0.1" | "argparse": "^2.0.1" | ||||
}, | }, | ||||
@@ -7135,6 +7254,7 @@ | |||||
"version": "1.52.0", | "version": "1.52.0", | ||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", | ||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", | ||||
"peer": true, | |||||
"engines": { | "engines": { | ||||
"node": ">= 0.6" | "node": ">= 0.6" | ||||
} | } | ||||
@@ -7143,6 +7263,7 @@ | |||||
"version": "2.1.35", | "version": "2.1.35", | ||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", | ||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", | ||||
"peer": true, | |||||
"dependencies": { | "dependencies": { | ||||
"mime-db": "1.52.0" | "mime-db": "1.52.0" | ||||
}, | }, | ||||
@@ -7183,6 +7304,25 @@ | |||||
"node": ">=16 || 14 >=14.17" | "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": { | "node_modules/ms": { | ||||
"version": "2.1.2", | "version": "2.1.2", | ||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", | ||||
@@ -7275,14 +7415,14 @@ | |||||
} | } | ||||
}, | }, | ||||
"node_modules/next-auth": { | "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": { | "dependencies": { | ||||
"@babel/runtime": "^7.20.13", | "@babel/runtime": "^7.20.13", | ||||
"@panva/hkdf": "^1.0.2", | "@panva/hkdf": "^1.0.2", | ||||
"cookie": "^0.5.0", | "cookie": "^0.5.0", | ||||
"jose": "^4.11.4", | |||||
"jose": "^4.15.5", | |||||
"oauth": "^0.9.15", | "oauth": "^0.9.15", | ||||
"openid-client": "^5.4.0", | "openid-client": "^5.4.0", | ||||
"preact": "^10.6.3", | "preact": "^10.6.3", | ||||
@@ -7993,6 +8133,14 @@ | |||||
"node": ">= 0.8.0" | "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": { | "node_modules/prettier": { | ||||
"version": "3.1.1", | "version": "3.1.1", | ||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", | ||||
@@ -8057,11 +8205,6 @@ | |||||
"react-is": "^16.13.1" | "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": { | "node_modules/punycode": { | ||||
"version": "2.3.1", | "version": "2.3.1", | ||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", | ||||
@@ -15,6 +15,7 @@ | |||||
"@faker-js/faker": "^8.4.1", | "@faker-js/faker": "^8.4.1", | ||||
"@fontsource/inter": "^5.0.16", | "@fontsource/inter": "^5.0.16", | ||||
"@fontsource/plus-jakarta-sans": "^5.0.18", | "@fontsource/plus-jakarta-sans": "^5.0.18", | ||||
"@fullcalendar/react": "^6.1.11", | |||||
"@mui/icons-material": "^5.15.0", | "@mui/icons-material": "^5.15.0", | ||||
"@mui/material": "^5.15.0", | "@mui/material": "^5.15.0", | ||||
"@mui/material-nextjs": "^5.15.0", | "@mui/material-nextjs": "^5.15.0", | ||||
@@ -22,12 +23,14 @@ | |||||
"@mui/x-date-pickers": "^6.18.7", | "@mui/x-date-pickers": "^6.18.7", | ||||
"@unly/universal-language-detector": "^2.0.3", | "@unly/universal-language-detector": "^2.0.3", | ||||
"apexcharts": "^3.45.2", | "apexcharts": "^3.45.2", | ||||
"date-holidays": "^3.23.11", | |||||
"dayjs": "^1.11.10", | "dayjs": "^1.11.10", | ||||
"fullcalendar": "^6.1.11", | |||||
"i18next": "^23.7.11", | "i18next": "^23.7.11", | ||||
"i18next-resources-to-backend": "^1.2.0", | "i18next-resources-to-backend": "^1.2.0", | ||||
"lodash": "^4.17.21", | "lodash": "^4.17.21", | ||||
"next": "14.0.4", | "next": "14.0.4", | ||||
"next-auth": "^4.24.5", | |||||
"next-auth": "^4.24.7", | |||||
"next-pwa": "^5.6.0", | "next-pwa": "^5.6.0", | ||||
"react": "^18", | "react": "^18", | ||||
"react-apexcharts": "^1.4.1", | "react-apexcharts": "^1.4.1", | ||||
@@ -2,10 +2,10 @@ import { Metadata } from "next"; | |||||
import { Suspense } from "react"; | import { Suspense } from "react"; | ||||
import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
import { fetchProjects } from "@/app/api/projects"; | import { fetchProjects } from "@/app/api/projects"; | ||||
import GenerateEX02ProjectCashFlowReport from "@/components/GenerateEX02ProjectCashFlowReport"; | |||||
import GenerateProjectCashFlowReport from "@/components/GenerateProjectCashFlowReport"; | |||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
title: "EX02 - Project Cash Flow Report", | |||||
title: "Project Cash Flow Report", | |||||
}; | }; | ||||
const ProjectCashFlowReport: React.FC = async () => { | const ProjectCashFlowReport: React.FC = async () => { | ||||
@@ -14,8 +14,8 @@ const ProjectCashFlowReport: React.FC = async () => { | |||||
return ( | return ( | ||||
<> | <> | ||||
<I18nProvider namespaces={["report", "common"]}> | <I18nProvider namespaces={["report", "common"]}> | ||||
<Suspense fallback={<GenerateEX02ProjectCashFlowReport.Loading />}> | |||||
<GenerateEX02ProjectCashFlowReport /> | |||||
<Suspense fallback={<GenerateProjectCashFlowReport.Loading />}> | |||||
<GenerateProjectCashFlowReport /> | |||||
</Suspense> | </Suspense> | ||||
</I18nProvider> | </I18nProvider> | ||||
</> | </> |
@@ -1,15 +1,36 @@ | |||||
import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
import UserWorkspacePage from "@/components/UserWorkspacePage"; | import UserWorkspacePage from "@/components/UserWorkspacePage"; | ||||
import { | |||||
fetchLeaveTypes, | |||||
fetchLeaves, | |||||
fetchTimesheets, | |||||
} from "@/app/api/timesheets"; | |||||
import { authOptions } from "@/config/authConfig"; | |||||
import { getServerSession } from "next-auth"; | |||||
import { | |||||
fetchAssignedProjects, | |||||
fetchProjectWithTasks, | |||||
} from "@/app/api/projects"; | |||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
title: "User Workspace", | title: "User Workspace", | ||||
}; | }; | ||||
const Home: React.FC = async () => { | const Home: React.FC = async () => { | ||||
const session = await getServerSession(authOptions); | |||||
// Get name for caching | |||||
const username = session!.user!.name!; | |||||
fetchTimesheets(username); | |||||
fetchAssignedProjects(username); | |||||
fetchLeaves(username); | |||||
fetchLeaveTypes(); | |||||
fetchProjectWithTasks(); | |||||
return ( | return ( | ||||
<I18nProvider namespaces={["home"]}> | <I18nProvider namespaces={["home"]}> | ||||
<UserWorkspacePage /> | |||||
<UserWorkspacePage username={username} /> | |||||
</I18nProvider> | </I18nProvider> | ||||
); | ); | ||||
}; | }; | ||||
@@ -5,7 +5,7 @@ import Button from "@mui/material/Button"; | |||||
import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
import Link from "next/link"; | import Link from "next/link"; | ||||
import CreateInvoice from "@/components/CreateInvoice"; | |||||
import CreateInvoice from "@/components/CreateInvoice_forGen"; | |||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
title: "Create Invoice", | title: "Create Invoice", | ||||
@@ -31,10 +31,10 @@ export default async function MainLayout({ | |||||
padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, | padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, | ||||
}} | }} | ||||
> | > | ||||
<Stack spacing={2}> | |||||
<Breadcrumb /> | |||||
{children} | |||||
</Stack> | |||||
<Stack spacing={2}> | |||||
<Breadcrumb /> | |||||
{children} | |||||
</Stack> | |||||
</Box> | </Box> | ||||
</> | </> | ||||
); | ); | ||||
@@ -43,7 +43,7 @@ const Projects: React.FC = async () => { | |||||
<> | <> | ||||
<Typography variant="h4">{t("Create Project")}</Typography> | <Typography variant="h4">{t("Create Project")}</Typography> | ||||
<I18nProvider namespaces={["projects"]}> | <I18nProvider namespaces={["projects"]}> | ||||
<CreateProject /> | |||||
<CreateProject isEditMode={false} /> | |||||
</I18nProvider> | </I18nProvider> | ||||
</> | </> | ||||
); | ); | ||||
@@ -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("projects", "common"); | |||||
return ( | |||||
<Stack spacing={2}> | |||||
<Typography variant="h4">{t("Not Found")}</Typography> | |||||
<Typography variant="body1">{t("The project was not found!")}</Typography> | |||||
<Link href="/projects" component={NextLink} variant="body2"> | |||||
{t("Return to all projects")} | |||||
</Link> | |||||
</Stack> | |||||
); | |||||
} |
@@ -0,0 +1,74 @@ | |||||
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | |||||
import { fetchGrades } from "@/app/api/grades"; | |||||
import { | |||||
fetchProjectBuildingTypes, | |||||
fetchProjectCategories, | |||||
fetchProjectContractTypes, | |||||
fetchProjectDetails, | |||||
fetchProjectFundingTypes, | |||||
fetchProjectLocationTypes, | |||||
fetchProjectServiceTypes, | |||||
fetchProjectWorkNatures, | |||||
} from "@/app/api/projects"; | |||||
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | |||||
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | |||||
import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||||
import CreateProject from "@/components/CreateProject"; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import { isArray } from "lodash"; | |||||
import { Metadata } from "next"; | |||||
import { notFound } from "next/navigation"; | |||||
interface Props { | |||||
searchParams: { [key: string]: string | string[] | undefined }; | |||||
} | |||||
export const metadata: Metadata = { | |||||
title: "Edit Project", | |||||
}; | |||||
const Projects: React.FC<Props> = async ({ searchParams }) => { | |||||
const { t } = await getServerI18n("projects"); | |||||
// Assume projectId is string here | |||||
const projectId = searchParams["id"]; | |||||
if (!projectId || isArray(projectId)) { | |||||
notFound(); | |||||
} | |||||
// Preload necessary dependencies | |||||
fetchAllTasks(); | |||||
fetchTaskTemplates(); | |||||
fetchProjectCategories(); | |||||
fetchProjectContractTypes(); | |||||
fetchProjectFundingTypes(); | |||||
fetchProjectLocationTypes(); | |||||
fetchProjectServiceTypes(); | |||||
fetchProjectBuildingTypes(); | |||||
fetchProjectWorkNatures(); | |||||
fetchAllCustomers(); | |||||
fetchAllSubsidiaries(); | |||||
fetchGrades(); | |||||
preloadTeamLeads(); | |||||
preloadStaff(); | |||||
try { | |||||
await fetchProjectDetails(projectId); | |||||
} catch (e) { | |||||
if (e instanceof ServerFetchError && e.response?.status === 404) { | |||||
notFound(); | |||||
} | |||||
} | |||||
return ( | |||||
<> | |||||
<Typography variant="h4">{t("Edit Project")}</Typography> | |||||
<I18nProvider namespaces={["projects"]}> | |||||
<CreateProject isEditMode projectId={projectId} /> | |||||
</I18nProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default Projects; |
@@ -0,0 +1,53 @@ | |||||
import { preloadClaims } from "@/app/api/claims"; | |||||
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | |||||
import ChangePassword from "@/components/ChangePassword"; | |||||
import StaffSearch from "@/components/StaffSearch"; | |||||
import TeamSearch from "@/components/TeamSearch"; | |||||
import UserGroupSearch from "@/components/UserGroupSearch"; | |||||
import UserSearch from "@/components/UserSearch"; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
import Add from "@mui/icons-material/Add"; | |||||
import Button from "@mui/material/Button"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import { Metadata } from "next"; | |||||
import Link from "next/link"; | |||||
import { Suspense } from "react"; | |||||
export const metadata: Metadata = { | |||||
title: "Change Password", | |||||
}; | |||||
const ChangePasswordPage: React.FC = async () => { | |||||
const { t } = await getServerI18n("User Group"); | |||||
// preloadTeamLeads(); | |||||
// preloadStaff(); | |||||
return ( | |||||
<> | |||||
<Stack | |||||
direction="row" | |||||
justifyContent="space-between" | |||||
flexWrap="wrap" | |||||
rowGap={2} | |||||
> | |||||
<Typography variant="h4" marginInlineEnd={2}> | |||||
{t("Change Password")} | |||||
</Typography> | |||||
</Stack> | |||||
{/* <I18nProvider namespaces={["User Group", "common"]}> | |||||
<Suspense fallback={<UserGroupSearch.Loading />}> | |||||
<UserGroupSearch /> | |||||
</Suspense> | |||||
</I18nProvider> */} | |||||
<I18nProvider namespaces={["User Group", "common"]}> | |||||
<Suspense fallback={<ChangePassword.Loading />}> | |||||
<ChangePassword /> | |||||
</Suspense> | |||||
</I18nProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default ChangePasswordPage; |
@@ -0,0 +1,31 @@ | |||||
import CreateDepartment from "@/components/CreateDepartment"; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import { Metadata } from "next"; | |||||
export const metadata: Metadata = { | |||||
title: "Create Department", | |||||
}; | |||||
interface Props { | |||||
searchParams: { [key: string]: string | undefined }; | |||||
} | |||||
const Department: React.FC<Props> = async ({searchParams}) => { | |||||
const { t } = await getServerI18n("departments"); | |||||
// Preload necessary dependencies | |||||
// Assume projectId is string here | |||||
const departmentId = searchParams["id"]; | |||||
return ( | |||||
<> | |||||
<Typography variant="h4">{t("Create Department")}</Typography> | |||||
<I18nProvider namespaces={["departments"]}> | |||||
<CreateDepartment isEdit={true} departmentId={departmentId}/> | |||||
</I18nProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default Department; |
@@ -16,7 +16,7 @@ const Department: React.FC = async () => { | |||||
<> | <> | ||||
<Typography variant="h4">{t("Create Department")}</Typography> | <Typography variant="h4">{t("Create Department")}</Typography> | ||||
<I18nProvider namespaces={["departments"]}> | <I18nProvider namespaces={["departments"]}> | ||||
<CreateDepartment /> | |||||
<CreateDepartment isEdit={false} /> | |||||
</I18nProvider> | </I18nProvider> | ||||
</> | </> | ||||
); | ); | ||||
@@ -0,0 +1,22 @@ | |||||
// 'use client'; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
import React, { useCallback, useState } from "react"; | |||||
import { Typography } from "@mui/material"; | |||||
import CreateGroup from "@/components/CreateGroup"; | |||||
// const Title = ["title1", "title2"]; | |||||
const CreateStaff: React.FC = async () => { | |||||
const { t } = await getServerI18n("group"); | |||||
return ( | |||||
<> | |||||
<Typography variant="h4">{t("Create Group")}</Typography> | |||||
<I18nProvider namespaces={["group"]}> | |||||
<CreateGroup /> | |||||
</I18nProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default CreateStaff; |
@@ -0,0 +1,26 @@ | |||||
import EditPosition from "@/components/EditPosition"; | |||||
import EditUserGroup from "@/components/EditUserGroup"; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import { Metadata } from "next"; | |||||
export const metadata: Metadata = { | |||||
title: "Edit User Group", | |||||
}; | |||||
const Group: React.FC = async () => { | |||||
const { t } = await getServerI18n("group"); | |||||
// Preload necessary dependencies | |||||
return ( | |||||
<> | |||||
{/* <Typography variant="h4">{t("Edit User Group")}</Typography> */} | |||||
<I18nProvider namespaces={["group"]}> | |||||
<EditUserGroup /> | |||||
</I18nProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default Group; |
@@ -0,0 +1,55 @@ | |||||
import { preloadClaims } from "@/app/api/claims"; | |||||
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | |||||
import StaffSearch from "@/components/StaffSearch"; | |||||
import TeamSearch from "@/components/TeamSearch"; | |||||
import UserGroupSearch from "@/components/UserGroupSearch"; | |||||
import UserSearch from "@/components/UserSearch"; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
import Add from "@mui/icons-material/Add"; | |||||
import Button from "@mui/material/Button"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import { Metadata } from "next"; | |||||
import Link from "next/link"; | |||||
import { Suspense } from "react"; | |||||
export const metadata: Metadata = { | |||||
title: "User Group", | |||||
}; | |||||
const UserGroup: React.FC = async () => { | |||||
const { t } = await getServerI18n("User Group"); | |||||
// preloadTeamLeads(); | |||||
// preloadStaff(); | |||||
return ( | |||||
<> | |||||
<Stack | |||||
direction="row" | |||||
justifyContent="space-between" | |||||
flexWrap="wrap" | |||||
rowGap={2} | |||||
> | |||||
<Typography variant="h4" marginInlineEnd={2}> | |||||
{t("User Group")} | |||||
</Typography> | |||||
<Button | |||||
variant="contained" | |||||
startIcon={<Add />} | |||||
LinkComponent={Link} | |||||
href="/settings/group/create" | |||||
> | |||||
{t("Create User Group")} | |||||
</Button> | |||||
</Stack> | |||||
<I18nProvider namespaces={["User Group", "common"]}> | |||||
<Suspense fallback={<UserGroupSearch.Loading />}> | |||||
<UserGroupSearch /> | |||||
</Suspense> | |||||
</I18nProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default UserGroup; |
@@ -0,0 +1,48 @@ | |||||
import CompanyHoliday from "@/components/CompanyHoliday"; | |||||
import { Metadata } from "next"; | |||||
import { getServerI18n } from "@/i18n"; | |||||
import Add from "@mui/icons-material/Add"; | |||||
import Button from "@mui/material/Button"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import Link from "next/link"; | |||||
import { Suspense } from "react"; | |||||
import { fetchCompanys, preloadCompanys } from "@/app/api/companys"; | |||||
export const metadata: Metadata = { | |||||
title: "Holiday", | |||||
}; | |||||
const Company: React.FC = async () => { | |||||
const { t } = await getServerI18n("holiday"); | |||||
// Preload necessary dependencies | |||||
return ( | |||||
<> | |||||
<Stack | |||||
direction="row" | |||||
justifyContent="space-between" | |||||
flexWrap="wrap" | |||||
rowGap={2} | |||||
> | |||||
<Typography variant="h4" marginInlineEnd={2}> | |||||
{t("Company Holiday")} | |||||
</Typography> | |||||
{/* <Button | |||||
variant="contained" | |||||
startIcon={<Add />} | |||||
LinkComponent={Link} | |||||
href="/settings/holiday/create" | |||||
> | |||||
{t("Create Holiday")} | |||||
</Button> */} | |||||
</Stack> | |||||
<Suspense fallback={<CompanyHoliday.Loading />}> | |||||
<CompanyHoliday/> | |||||
</Suspense> | |||||
</> | |||||
) | |||||
}; | |||||
export default Company; |
@@ -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 ( | |||||
<> | |||||
<Typography variant="h4">{t("Edit Skill")}</Typography> | |||||
<I18nProvider namespaces={["team", "common"]}> | |||||
<Suspense fallback={<EditSkill.Loading />}> | |||||
<EditSkill /> | |||||
</Suspense> | |||||
</I18nProvider> | |||||
{/* <EditStaff /> */} | |||||
</> | |||||
); | |||||
}; | |||||
export default EditSkillPage; |
@@ -22,7 +22,7 @@ import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions"; | |||||
import { Error } from "@mui/icons-material"; | import { Error } from "@mui/icons-material"; | ||||
import { ProjectCategory } from "@/app/api/projects"; | import { ProjectCategory } from "@/app/api/projects"; | ||||
import { Grid, Typography } from "@mui/material"; | import { Grid, Typography } from "@mui/material"; | ||||
import CreateStaffForm from "@/components/CreateStaff/CreateStaff"; | |||||
import CreateStaff from "@/components/CreateStaff"; | |||||
interface CreateCustomInputs { | interface CreateCustomInputs { | ||||
projectCode: string; | projectCode: string; | ||||
@@ -31,23 +31,17 @@ interface CreateCustomInputs { | |||||
// const Title = ["title1", "title2"]; | // const Title = ["title1", "title2"]; | ||||
const CreateStaff: React.FC = async () => { | |||||
const CreateStaffPage: React.FC = async () => { | |||||
const { t } = await getServerI18n("staff"); | const { t } = await getServerI18n("staff"); | ||||
const title = ['', t('Additional Info')] | |||||
// const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$") | |||||
// console.log(regex) | |||||
return ( | return ( | ||||
<> | <> | ||||
<Typography variant="h4">{t("Create Staff")}</Typography> | <Typography variant="h4">{t("Create Staff")}</Typography> | ||||
<I18nProvider namespaces={["staff"]}> | <I18nProvider namespaces={["staff"]}> | ||||
<CreateStaffForm | |||||
Title={title} | |||||
/> | |||||
<CreateStaff/> | |||||
</I18nProvider> | </I18nProvider> | ||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
export default CreateStaff; | |||||
export default CreateStaffPage; |
@@ -0,0 +1,22 @@ | |||||
import { Edit } from "@mui/icons-material"; | |||||
import { Metadata } from "next"; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
import EditUser from "@/components/EditUser"; | |||||
import { Typography } from "@mui/material"; | |||||
import { Suspense } from "react"; | |||||
const User: React.FC = async () => { | |||||
const { t } = await getServerI18n("user"); | |||||
return ( | |||||
<> | |||||
<Typography variant="h4">{t("Edit User")}</Typography> | |||||
<I18nProvider namespaces={["user", "common"]}> | |||||
<Suspense fallback={<EditUser.Loading />}> | |||||
<EditUser /> | |||||
</Suspense> | |||||
</I18nProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default User; |
@@ -28,10 +28,6 @@ import CreateTeam from "@/components/CreateTeam"; | |||||
const CreateTeamPage: React.FC = async () => { | const CreateTeamPage: React.FC = async () => { | ||||
const { t } = await getServerI18n("team"); | const { t } = await getServerI18n("team"); | ||||
const title = ['', t('Additional Info')] | |||||
// const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$") | |||||
// console.log(regex) | |||||
return ( | return ( | ||||
<> | <> | ||||
<Typography variant="h4">{t("Create Team")}</Typography> | <Typography variant="h4">{t("Create Team")}</Typography> | ||||
@@ -0,0 +1,24 @@ | |||||
import { Edit } from "@mui/icons-material"; | |||||
import { useSearchParams } from "next/navigation"; | |||||
// import EditStaff from "@/components/EditStaff"; | |||||
import { Suspense } from "react"; | |||||
import { I18nProvider } from "@/i18n"; | |||||
// import EditStaffWrapper from "@/components/EditStaff/EditStaffWrapper"; | |||||
import { Metadata } from "next"; | |||||
import EditUser from "@/components/EditUser"; | |||||
const EditUserPage: React.FC = () => { | |||||
return ( | |||||
<> | |||||
<I18nProvider namespaces={["team", "common"]}> | |||||
<Suspense fallback={<EditUser.Loading />}> | |||||
<EditUser /> | |||||
</Suspense> | |||||
</I18nProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default EditUserPage; |
@@ -33,14 +33,14 @@ export const metadata: Metadata = { | |||||
<Typography variant="h4" marginInlineEnd={2}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
{t("User")} | {t("User")} | ||||
</Typography> | </Typography> | ||||
<Button | |||||
{/* <Button | |||||
variant="contained" | variant="contained" | ||||
startIcon={<Add />} | startIcon={<Add />} | ||||
LinkComponent={Link} | LinkComponent={Link} | ||||
href="/settings/team/create" | href="/settings/team/create" | ||||
> | > | ||||
{t("Create User")} | {t("Create User")} | ||||
</Button> | |||||
</Button> */} | |||||
</Stack> | </Stack> | ||||
<I18nProvider namespaces={["User", "common"]}> | <I18nProvider namespaces={["User", "common"]}> | ||||
<Suspense fallback={<UserSearch.Loading />}> | <Suspense fallback={<UserSearch.Loading />}> | ||||
@@ -1,4 +1,4 @@ | |||||
import ClaimDetail from "@/components/ClaimDetail"; | |||||
import ClaimSave from "@/components/ClaimSave"; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | import { I18nProvider, getServerI18n } from "@/i18n"; | ||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
@@ -14,7 +14,7 @@ const ClaimDetails: React.FC = async () => { | |||||
<> | <> | ||||
<Typography variant="h4">{t("Create Claim")}</Typography> | <Typography variant="h4">{t("Create Claim")}</Typography> | ||||
<I18nProvider namespaces={["claim", "common"]}> | <I18nProvider namespaces={["claim", "common"]}> | ||||
<ClaimDetail /> | |||||
<ClaimSave /> | |||||
</I18nProvider> | </I18nProvider> | ||||
</> | </> | ||||
); | ); | ||||
@@ -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 ( | |||||
<Stack spacing={2}> | |||||
<Typography variant="h4">{t("Not Found")}</Typography> | |||||
<Typography variant="body1">{t("The task template was not found!")}</Typography> | |||||
<Link href="/projects" component={NextLink} variant="body2"> | |||||
{t("Return to all task templates")} | |||||
</Link> | |||||
</Stack> | |||||
); | |||||
} |
@@ -1,23 +1,44 @@ | |||||
import { preloadAllTasks } from "@/app/api/tasks"; | |||||
import { fetchTaskTemplateDetail, preloadAllTasks } from "@/app/api/tasks"; | |||||
import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | ||||
import { getServerI18n } from "@/i18n"; | import { getServerI18n } from "@/i18n"; | ||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||||
import { isArray } from "lodash"; | |||||
import { notFound } from "next/navigation"; | |||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
title: "Edit Task Template", | title: "Edit Task Template", | ||||
}; | }; | ||||
const TaskTemplates: React.FC = async () => { | |||||
interface Props { | |||||
searchParams: { [key: string]: string | string[] | undefined }; | |||||
} | |||||
const TaskTemplates: React.FC<Props> = async ({ searchParams }) => { | |||||
const { t } = await getServerI18n("tasks"); | const { t } = await getServerI18n("tasks"); | ||||
const taskTemplateId = searchParams["id"]; | |||||
if (!taskTemplateId || isArray(taskTemplateId)) { | |||||
notFound(); | |||||
} | |||||
preloadAllTasks(); | preloadAllTasks(); | ||||
try { | |||||
await fetchTaskTemplateDetail(taskTemplateId); | |||||
} catch (e) { | |||||
if (e instanceof ServerFetchError && e.response?.status === 404) { | |||||
notFound(); | |||||
} | |||||
} | |||||
return ( | return ( | ||||
<> | <> | ||||
<Typography variant="h4">{t("Edit Task Template")}</Typography> | <Typography variant="h4">{t("Edit Task Template")}</Typography> | ||||
<I18nProvider namespaces={["tasks", "common"]}> | <I18nProvider namespaces={["tasks", "common"]}> | ||||
<CreateTaskTemplate /> | |||||
<CreateTaskTemplate taskTemplateId={taskTemplateId}/> | |||||
</I18nProvider> | </I18nProvider> | ||||
</> | </> | ||||
); | ); | ||||
@@ -1,6 +1,6 @@ | |||||
"use server" | "use server" | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
@@ -14,8 +14,9 @@ export interface combo { | |||||
records: comboProp[]; | records: comboProp[]; | ||||
} | } | ||||
export interface CreateDepartmentInputs { | export interface CreateDepartmentInputs { | ||||
departmentCode: string; | |||||
departmentName: string; | |||||
id: number; | |||||
code: string; | |||||
name: string; | |||||
description: string; | description: string; | ||||
} | } | ||||
@@ -25,7 +26,19 @@ export const saveDepartment = async (data: CreateDepartmentInputs) => { | |||||
body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
headers: { "Content-Type": "application/json" }, | headers: { "Content-Type": "application/json" }, | ||||
}); | }); | ||||
}; | |||||
}; | |||||
export const deleteDepartment = async (id: number) => { | |||||
const department = await serverFetchWithNoContent( | |||||
`${BASE_API_URL}/departments/${id}`, | |||||
{ | |||||
method: "DELETE", | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
return department | |||||
}; | |||||
export const fetchDepartmentCombo = cache(async () => { | export const fetchDepartmentCombo = cache(async () => { | ||||
@@ -2,6 +2,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import "server-only"; | import "server-only"; | ||||
import { CreateDepartmentInputs } from "./actions"; | |||||
export interface DepartmentResult { | export interface DepartmentResult { | ||||
id: number; | id: number; | ||||
@@ -18,4 +19,13 @@ export const fetchDepartments = cache(async () => { | |||||
return serverFetchJson<DepartmentResult[]>(`${BASE_API_URL}/departments`, { | return serverFetchJson<DepartmentResult[]>(`${BASE_API_URL}/departments`, { | ||||
next: { tags: ["departments"] }, | next: { tags: ["departments"] }, | ||||
}); | }); | ||||
}); | |||||
export const fetchDepartmentDetails = cache(async (departmentId: string) => { | |||||
return serverFetchJson<CreateDepartmentInputs>( | |||||
`${BASE_API_URL}/departments/departmentDetails/${departmentId}`, | |||||
{ | |||||
next: { tags: [`departmentDetail${departmentId}`] }, | |||||
}, | |||||
); | |||||
}); | }); |
@@ -0,0 +1,51 @@ | |||||
"use server"; | |||||
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | |||||
import { revalidateTag } from "next/cache"; | |||||
import { cache } from "react"; | |||||
export interface CreateGroupInputs { | |||||
id?: number; | |||||
name: string; | |||||
description: string; | |||||
addUserIds?: number[]; | |||||
removeUserIds?: number[]; | |||||
addAuthIds?: number[]; | |||||
removeAuthIds?: number[]; | |||||
} | |||||
export interface auth { | |||||
id: number; | |||||
module?: any | null; | |||||
authority: string; | |||||
name: string; | |||||
description: string | null; | |||||
v: number; | |||||
} | |||||
export interface record { | |||||
records: auth[]; | |||||
} | |||||
export const fetchAuth = cache(async (target: string, id?: number) => { | |||||
return serverFetchJson<record>(`${BASE_API_URL}/group/auth/${target}/${id ?? 0}`, { | |||||
next: { tags: ["auth"] }, | |||||
}); | |||||
}); | |||||
export const saveGroup = async (data: CreateGroupInputs) => { | |||||
return serverFetchJson(`${BASE_API_URL}/group/save`, { | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}); | |||||
}; | |||||
export const deleteGroup = async (id: number) => { | |||||
return serverFetchWithNoContent(`${BASE_API_URL}/group/${id}`, { | |||||
method: "DELETE", | |||||
headers: { "Content-Type": "application/json" }, | |||||
}); | |||||
}; |
@@ -0,0 +1,21 @@ | |||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | |||||
import { cache } from "react"; | |||||
import "server-only"; | |||||
export interface Records { | |||||
records: UserGroupResult[] | |||||
} | |||||
export interface UserGroupResult { | |||||
id: number; | |||||
action: () => void; | |||||
name: string; | |||||
description: string; | |||||
} | |||||
export const fetchGroup = cache(async () => { | |||||
return serverFetchJson<Records>(`${BASE_API_URL}/group`, { | |||||
next: { tags: ["group"] }, | |||||
}); | |||||
}); |
@@ -0,0 +1,33 @@ | |||||
"use server"; | |||||
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | |||||
import { Dayjs } from "dayjs"; | |||||
import { cache } from "react"; | |||||
export interface CreateCompanyHolidayInputs { | |||||
id: number; | |||||
name: string; | |||||
date: string; | |||||
} | |||||
export const saveCompanyHoliday = async (data: CreateCompanyHolidayInputs) => { | |||||
return serverFetchJson(`${BASE_API_URL}/company-holidays/new`, { | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}); | |||||
}; | |||||
export const deleteCompanyHoliday = async (id: number) => { | |||||
const holiday = await serverFetchWithNoContent( | |||||
`${BASE_API_URL}/company-holidays/${id}`, | |||||
{ | |||||
method: "DELETE", | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
return holiday | |||||
}; | |||||
@@ -0,0 +1,30 @@ | |||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | |||||
import { cache } from "react"; | |||||
import "server-only"; | |||||
import EventInput from '@fullcalendar/react'; | |||||
export interface HolidaysList extends EventInput { | |||||
id: string; | |||||
title: string; | |||||
date: string; | |||||
extendedProps: { | |||||
calendar: string; | |||||
}; | |||||
} | |||||
export interface HolidaysResult { | |||||
id: string; | |||||
name: string; | |||||
date: number[]; | |||||
} | |||||
export const preloadCompanys = () => { | |||||
fetchHolidays(); | |||||
}; | |||||
export const fetchHolidays = cache(async () => { | |||||
return serverFetchJson<HolidaysResult[]>(`${BASE_API_URL}/company-holidays`, { | |||||
next: { tags: ["company-holidays"] }, | |||||
}); | |||||
}); |
@@ -1,6 +1,6 @@ | |||||
"use server" | "use server" | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { serverFetchJson, serverFetchString } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
@@ -64,4 +64,32 @@ export const fetchInvoiceInfoById = cache(async (id: number) => { | |||||
return serverFetchJson<InvoiceInformation[]>(`${BASE_API_URL}/invoices/getInvoiceInfo/${id}`, { | return serverFetchJson<InvoiceInformation[]>(`${BASE_API_URL}/invoices/getInvoiceInfo/${id}`, { | ||||
next: { tags: ["invoiceInfoById"] }, | next: { tags: ["invoiceInfoById"] }, | ||||
}); | }); | ||||
}) | |||||
}) | |||||
export const importIssuedInovice = async (data: FormData) => { | |||||
// console.log("----------------",data) | |||||
const importIssuedInovice = await serverFetchJson<any>( | |||||
`${BASE_API_URL}/invoices/import/issued`, | |||||
{ | |||||
method: "POST", | |||||
body: data, | |||||
// headers: { "Content-Type": "multipart/form-data" }, | |||||
}, | |||||
); | |||||
return importIssuedInovice; | |||||
}; | |||||
export const importReceivedInovice = async (data: FormData) => { | |||||
// console.log("----------------",data) | |||||
const importReceivedInovice = await serverFetchJson<any>( | |||||
`${BASE_API_URL}/invoices/import/received`, | |||||
{ | |||||
method: "POST", | |||||
body: data, | |||||
// headers: { "Content-Type": "multipart/form-data" }, | |||||
}, | |||||
); | |||||
return importReceivedInovice; | |||||
}; |
@@ -15,6 +15,107 @@ export interface InvoiceResult { | |||||
reminder: string; | reminder: string; | ||||
} | } | ||||
export interface issuedInvoiceResult { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
team: string; | |||||
stage: string; | |||||
paymentMilestone: string; | |||||
paymentMilestoneDate: string; | |||||
client: string; | |||||
address: string; | |||||
attention: string; | |||||
invoiceDate: number[]; | |||||
dueDate: number[]; | |||||
issuedAmount: number; | |||||
} | |||||
export interface receivedInvoiceResult { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
team: string; | |||||
receiptDate: number[]; | |||||
receivedAmount: number; | |||||
} | |||||
export interface issuedInvoiceList { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
// team: string; | |||||
stage: string; | |||||
paymentMilestone: string; | |||||
// paymentMilestoneDate: string; | |||||
// client: string; | |||||
// address: string; | |||||
// attention: string; | |||||
invoiceDate: string; | |||||
dueDate: string; | |||||
issuedAmount: string; | |||||
} | |||||
export interface receivedInvoiceList { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
team: string; | |||||
// stage: string; | |||||
// paymentMilestone: string; | |||||
// paymentMilestoneDate: string; | |||||
// client: string; | |||||
// address: string; | |||||
// attention: string; | |||||
receiptDate: string; | |||||
receivedAmount: string; | |||||
} | |||||
export interface issuedInvoiceSearchForm { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
// team: string; | |||||
// stage: string; | |||||
// paymentMilestone: string; | |||||
// paymentMilestoneDate: string; | |||||
// client: string; | |||||
// address: string; | |||||
// attention: string; | |||||
invoiceDate: string; | |||||
invoiceDateTo: string; | |||||
dueDate: string; | |||||
dueDateTo: string; | |||||
// issuedAmount: string; | |||||
} | |||||
export interface receivedInvoiceSearchForm { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
// team: string; | |||||
// stage: string; | |||||
// paymentMilestone: string; | |||||
// paymentMilestoneDate: string; | |||||
// client: string; | |||||
// address: string; | |||||
// attention: string; | |||||
receiptDate: string; | |||||
receiptDateTo: string; | |||||
// dueDate: string; | |||||
// dueDateTo: string; | |||||
// issuedAmount: string; | |||||
} | |||||
export interface InvoiceInformatio{ | export interface InvoiceInformatio{ | ||||
id: number; | id: number; | ||||
address: string; | address: string; | ||||
@@ -32,4 +133,16 @@ export const fetchInvoices = cache(async () => { | |||||
return serverFetchJson<InvoiceResult[]>(`${BASE_API_URL}/invoices`, { | return serverFetchJson<InvoiceResult[]>(`${BASE_API_URL}/invoices`, { | ||||
next: { tags: ["invoices"] }, | next: { tags: ["invoices"] }, | ||||
}); | }); | ||||
}); | |||||
export const fetchIssuedInvoices = cache(async () => { | |||||
return serverFetchJson<issuedInvoiceResult[]>(`${BASE_API_URL}/invoices/v2/allInvoices`, { | |||||
next: { tags: ["invoices"] }, | |||||
}); | |||||
}); | |||||
export const fetchReceivedInvoices = cache(async () => { | |||||
return serverFetchJson<receivedInvoiceResult[]>(`${BASE_API_URL}/invoices/v2/allInvoices/paid`, { | |||||
next: { tags: ["invoices"] }, | |||||
}); | |||||
}); | }); |
@@ -1,6 +1,6 @@ | |||||
"use server" | "use server" | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import { PositionResult } from "."; | import { PositionResult } from "."; | ||||
@@ -17,13 +17,15 @@ export interface combo { | |||||
export interface CreatePositionInputs { | export interface CreatePositionInputs { | ||||
positionCode: string; | positionCode: string; | ||||
positionName: string; | positionName: string; | ||||
code: string; | |||||
name: string; | |||||
description: string; | description: string; | ||||
} | } | ||||
export interface EditPositionInputs { | export interface EditPositionInputs { | ||||
id: number; | id: number; | ||||
positionCode: string; | |||||
positionName: string; | |||||
code: string; | |||||
name: string; | |||||
description: string; | description: string; | ||||
} | } | ||||
@@ -35,13 +37,25 @@ export const savePosition = async (data: CreatePositionInputs) => { | |||||
}); | }); | ||||
}; | }; | ||||
export const editPosition = async (data: EditPositionInputs) => { | |||||
return serverFetchJson(`${BASE_API_URL}/positions/new`, { | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}); | |||||
}; | |||||
export const editPosition = async (data: EditPositionInputs) => { | |||||
return serverFetchJson(`${BASE_API_URL}/positions/new`, { | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}); | |||||
}; | |||||
export const deletePosition = async (id: number) => { | |||||
const position = await serverFetchWithNoContent( | |||||
`${BASE_API_URL}/positions/${id}`, | |||||
{ | |||||
method: "DELETE", | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
return position | |||||
}; | |||||
export const fetchPositionCombo = cache(async () => { | export const fetchPositionCombo = cache(async () => { | ||||
return serverFetchJson<combo>(`${BASE_API_URL}/positions/combo`, { | return serverFetchJson<combo>(`${BASE_API_URL}/positions/combo`, { | ||||
@@ -1,17 +1,25 @@ | |||||
"use server"; | "use server"; | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { | |||||
serverFetchJson, | |||||
serverFetchWithNoContent, | |||||
} from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { Task, TaskGroup } from "../tasks"; | import { Task, TaskGroup } from "../tasks"; | ||||
import { Customer } from "../customer"; | import { Customer } from "../customer"; | ||||
import { revalidatePath, revalidateTag } from "next/cache"; | |||||
export interface CreateProjectInputs { | export interface CreateProjectInputs { | ||||
// Project details | |||||
// Project | |||||
projectId: number | null; | |||||
projectDeleted: boolean | null; | |||||
projectCode: string; | projectCode: string; | ||||
projectName: string; | projectName: string; | ||||
projectCategoryId: number; | projectCategoryId: number; | ||||
projectDescription: string; | projectDescription: string; | ||||
projectLeadId: number; | projectLeadId: number; | ||||
projectActualStart: string; | |||||
projectActualEnd: string; | |||||
// Project info | // Project info | ||||
serviceTypeId: number; | serviceTypeId: number; | ||||
@@ -61,10 +69,38 @@ export interface PaymentInputs { | |||||
amount: number; | amount: number; | ||||
} | } | ||||
export interface CreateProjectResponse { | |||||
id: number; | |||||
name: string; | |||||
code: string; | |||||
category: string; | |||||
team: string; | |||||
client: string; | |||||
} | |||||
export const saveProject = async (data: CreateProjectInputs) => { | export const saveProject = async (data: CreateProjectInputs) => { | ||||
return serverFetchJson(`${BASE_API_URL}/projects/new`, { | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}); | |||||
const newProject = await serverFetchJson<CreateProjectResponse>( | |||||
`${BASE_API_URL}/projects/new`, | |||||
{ | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
revalidateTag("projects"); | |||||
return newProject; | |||||
}; | |||||
export const deleteProject = async (id: number) => { | |||||
const project = await serverFetchWithNoContent( | |||||
`${BASE_API_URL}/projects/${id}`, | |||||
{ | |||||
method: "DELETE", | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
revalidateTag("projects"); | |||||
revalidatePath("/(main)/home"); | |||||
return project; | |||||
}; | }; |
@@ -3,6 +3,7 @@ import { BASE_API_URL } from "@/config/api"; | |||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import "server-only"; | import "server-only"; | ||||
import { Task, TaskGroup } from "../tasks"; | import { Task, TaskGroup } from "../tasks"; | ||||
import { CreateProjectInputs } from "./actions"; | |||||
export interface ProjectResult { | export interface ProjectResult { | ||||
id: number; | id: number; | ||||
@@ -48,17 +49,20 @@ export interface WorkNature { | |||||
name: string; | name: string; | ||||
} | } | ||||
export interface AssignedProject { | |||||
export interface ProjectWithTasks { | |||||
id: number; | id: number; | ||||
code: string; | code: string; | ||||
name: string; | name: string; | ||||
tasks: Task[]; | tasks: Task[]; | ||||
milestones: { | milestones: { | ||||
[taskGroupId: TaskGroup["id"]]: { | [taskGroupId: TaskGroup["id"]]: { | ||||
startDate: string; | |||||
endDate: string; | |||||
startDate?: string; | |||||
endDate?: string; | |||||
}; | }; | ||||
}; | }; | ||||
} | |||||
export interface AssignedProject extends ProjectWithTasks { | |||||
// Manhour info | // Manhour info | ||||
hoursSpent: number; | hoursSpent: number; | ||||
hoursSpentOther: number; | hoursSpentOther: number; | ||||
@@ -137,11 +141,29 @@ export const fetchProjectWorkNatures = cache(async () => { | |||||
}); | }); | ||||
}); | }); | ||||
export const fetchAssignedProjects = cache(async () => { | |||||
export const fetchAssignedProjects = cache(async (username: string) => { | |||||
return serverFetchJson<AssignedProject[]>( | return serverFetchJson<AssignedProject[]>( | ||||
`${BASE_API_URL}/projects/assignedProjects`, | `${BASE_API_URL}/projects/assignedProjects`, | ||||
{ | { | ||||
next: { tags: ["assignedProjects"] }, | |||||
next: { tags: [`assignedProjects__${username}`] }, | |||||
}, | |||||
); | |||||
}); | |||||
export const fetchProjectWithTasks = cache(async () => { | |||||
return serverFetchJson<ProjectWithTasks[]>( | |||||
`${BASE_API_URL}/projects/allProjectWithTasks`, | |||||
{ | |||||
next: { tags: ["allProjectWithTasks"] }, | |||||
}, | |||||
); | |||||
}); | |||||
export const fetchProjectDetails = cache(async (projectId: string) => { | |||||
return serverFetchJson<CreateProjectInputs>( | |||||
`${BASE_API_URL}/projects/projectDetails/${projectId}`, | |||||
{ | |||||
next: { tags: [`projectDetails_${projectId}`] }, | |||||
}, | }, | ||||
); | ); | ||||
}); | }); |
@@ -1,7 +1,7 @@ | |||||
"use server"; | "use server"; | ||||
import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; | import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; | ||||
import { EX02ProjectCashFlowReportRequest } from "."; | |||||
import { ProjectCashFlowReportRequest } from "."; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
export interface FileResponse { | export interface FileResponse { | ||||
@@ -9,9 +9,9 @@ export interface FileResponse { | |||||
blobValue: Uint8Array; | blobValue: Uint8Array; | ||||
} | } | ||||
export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowReportRequest) => { | |||||
export const fetchProjectCashFlowReport = async (data: ProjectCashFlowReportRequest) => { | |||||
const reportBlob = await serverFetchBlob<FileResponse>( | const reportBlob = await serverFetchBlob<FileResponse>( | ||||
`${BASE_API_URL}/reports/EX02-ProjectCashFlowReport`, | |||||
`${BASE_API_URL}/reports/ProjectCashFlowReport`, | |||||
{ | { | ||||
method: "POST", | method: "POST", | ||||
body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
@@ -1,8 +1,8 @@ | |||||
// EX02 - Project Cash Flow Report | |||||
export interface EX02ProjectCashFlowReportFilter { | |||||
// - Project Cash Flow Report | |||||
export interface ProjectCashFlowReportFilter { | |||||
project: string[]; | project: string[]; | ||||
} | } | ||||
export interface EX02ProjectCashFlowReportRequest { | |||||
export interface ProjectCashFlowReportRequest { | |||||
projectId: number; | projectId: number; | ||||
} | } |
@@ -1,8 +1,9 @@ | |||||
"use server" | "use server" | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { serverFetchBlob, serverFetchJson, serverFetchString } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import { FileResponse } from "../reports/actions"; | |||||
export interface comboProp { | export interface comboProp { | ||||
id: any; | id: any; | ||||
@@ -17,4 +18,31 @@ export const fetchSalaryCombo = cache(async () => { | |||||
return serverFetchJson<combo>(`${BASE_API_URL}/salarys/combo`, { | return serverFetchJson<combo>(`${BASE_API_URL}/salarys/combo`, { | ||||
next: { tags: ["salary"] }, | next: { tags: ["salary"] }, | ||||
}); | }); | ||||
}); | |||||
}); | |||||
export const importSalarys = async (data: FormData) => { | |||||
console.log("----------------",data) | |||||
const importSalarys = await serverFetchString<String>( | |||||
`${BASE_API_URL}/salarys/import`, | |||||
{ | |||||
method: "POST", | |||||
body: data, | |||||
// headers: { "Content-Type": "multipart/form-data" }, | |||||
}, | |||||
); | |||||
return importSalarys; | |||||
}; | |||||
export const exportSalary = async () => { | |||||
const reportBlob = await serverFetchBlob<FileResponse>( | |||||
`${BASE_API_URL}/salarys/export`, | |||||
{ | |||||
method: "POST", | |||||
// body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
return reportBlob | |||||
}; |
@@ -4,13 +4,26 @@ import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { TaskTemplate } from "."; | import { TaskTemplate } from "."; | ||||
import { revalidateTag } from "next/cache"; | import { revalidateTag } from "next/cache"; | ||||
import { ManhourAllocation } from "@/app/api/projects/actions"; | |||||
import { Task, TaskGroup } from '@/app/api/tasks'; | |||||
export interface NewTaskTemplateFormInputs { | export interface NewTaskTemplateFormInputs { | ||||
// task template | |||||
code: string; | code: string; | ||||
name: string; | name: string; | ||||
taskIds: number[]; | taskIds: number[]; | ||||
id: number | null; | id: number | null; | ||||
// resource allocation template | |||||
manhourPercentageByGrade: ManhourAllocation; | |||||
taskGroups: { | |||||
[taskGroup: TaskGroup["id"]]: { | |||||
taskIds: Task["id"][]; | |||||
percentAllocation: number; | |||||
}; | |||||
}; | |||||
} | } | ||||
export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => { | export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => { | ||||
@@ -2,6 +2,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import "server-only"; | import "server-only"; | ||||
import { NewTaskTemplateFormInputs } from "./actions"; | |||||
export interface TaskGroup { | export interface TaskGroup { | ||||
id: number; | id: number; | ||||
@@ -39,3 +40,15 @@ export const preloadAllTasks = () => { | |||||
export const fetchAllTasks = cache(async () => { | export const fetchAllTasks = cache(async () => { | ||||
return serverFetchJson<Task[]>(`${BASE_API_URL}/tasks`); | return serverFetchJson<Task[]>(`${BASE_API_URL}/tasks`); | ||||
}); | }); | ||||
export const fetchTaskTemplateDetail = cache(async (id: string) => { | |||||
const taskTemplate = await serverFetchJson<NewTaskTemplateFormInputs>( | |||||
`${BASE_API_URL}/tasks/templatesDetails/${id}`, | |||||
{ | |||||
method: "GET", | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
return taskTemplate; | |||||
}); |
@@ -15,6 +15,7 @@ export interface TeamResult { | |||||
staffName: string; | staffName: string; | ||||
posLabel: string; | posLabel: string; | ||||
posCode: string; | posCode: string; | ||||
teamLead: number; | |||||
} | } | ||||
@@ -1,15 +1,65 @@ | |||||
"use server"; | "use server"; | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { ProjectResult } from "../projects"; | import { ProjectResult } from "../projects"; | ||||
import { Task, TaskGroup } from "../tasks"; | import { Task, TaskGroup } from "../tasks"; | ||||
import { BASE_API_URL } from "@/config/api"; | |||||
import { revalidateTag } from "next/cache"; | |||||
export interface TimeEntry { | export interface TimeEntry { | ||||
projectId: ProjectResult["id"]; | |||||
taskGroupId: TaskGroup["id"]; | |||||
taskId: Task["id"]; | |||||
inputHours: number; | |||||
id: number; | |||||
projectId?: ProjectResult["id"]; | |||||
taskGroupId?: TaskGroup["id"]; | |||||
taskId?: Task["id"]; | |||||
inputHours?: number; | |||||
otHours?: number; | |||||
remark?: string; | |||||
} | } | ||||
export interface RecordTimesheetInput { | export interface RecordTimesheetInput { | ||||
[date: string]: TimeEntry[]; | [date: string]: TimeEntry[]; | ||||
} | } | ||||
export interface LeaveEntry { | |||||
id: number; | |||||
inputHours: number; | |||||
leaveTypeId: number; | |||||
remark?: string; | |||||
} | |||||
export interface RecordLeaveInput { | |||||
[date: string]: LeaveEntry[]; | |||||
} | |||||
export const saveTimesheet = async ( | |||||
data: RecordTimesheetInput, | |||||
username: string, | |||||
) => { | |||||
const savedRecords = await serverFetchJson<RecordTimesheetInput>( | |||||
`${BASE_API_URL}/timesheets/save`, | |||||
{ | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
revalidateTag(`timesheets_${username}`); | |||||
return savedRecords; | |||||
}; | |||||
export const saveLeave = async (data: RecordLeaveInput, username: string) => { | |||||
const savedRecords = await serverFetchJson<RecordLeaveInput>( | |||||
`${BASE_API_URL}/timesheets/saveLeave`, | |||||
{ | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}, | |||||
); | |||||
revalidateTag(`leaves_${username}`); | |||||
return savedRecords; | |||||
}; |
@@ -0,0 +1,30 @@ | |||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | |||||
import { cache } from "react"; | |||||
import { RecordLeaveInput, RecordTimesheetInput } from "./actions"; | |||||
export interface LeaveType { | |||||
id: number; | |||||
name: string; | |||||
} | |||||
export const fetchTimesheets = cache(async (username: string) => { | |||||
return serverFetchJson<RecordTimesheetInput>(`${BASE_API_URL}/timesheets`, { | |||||
next: { tags: [`timesheets_${username}`] }, | |||||
}); | |||||
}); | |||||
export const fetchLeaves = cache(async (username: string) => { | |||||
return serverFetchJson<RecordLeaveInput>( | |||||
`${BASE_API_URL}/timesheets/leaves`, | |||||
{ | |||||
next: { tags: [`leaves_${username}`] }, | |||||
}, | |||||
); | |||||
}); | |||||
export const fetchLeaveTypes = cache(async () => { | |||||
return serverFetchJson<LeaveType[]>(`${BASE_API_URL}/timesheets/leaveTypes`, { | |||||
next: { tags: ["leaveTypes"] }, | |||||
}); | |||||
}); |
@@ -0,0 +1,49 @@ | |||||
import { LeaveEntry, TimeEntry } from "./actions"; | |||||
/** | |||||
* @param entry - the time entry | |||||
* @returns the field where there is an error, or an empty string if there is none | |||||
*/ | |||||
export const isValidTimeEntry = (entry: Partial<TimeEntry>): 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.remark) { | |||||
error = "remark"; | |||||
} | |||||
} | |||||
return error; | |||||
}; | |||||
export const isValidLeaveEntry = (entry: Partial<LeaveEntry>): string => { | |||||
// Test for errrors | |||||
let error: keyof LeaveEntry | "" = ""; | |||||
if (!entry.leaveTypeId) { | |||||
error = "leaveTypeId"; | |||||
} else if (!entry.inputHours || !(entry.inputHours >= 0)) { | |||||
error = "inputHours"; | |||||
} | |||||
return error; | |||||
}; | |||||
export const LEAVE_DAILY_MAX_HOURS = 8; | |||||
export const TIMESHEET_DAILY_MAX_HOURS = 20; |
@@ -7,9 +7,16 @@ import { UserDetail, UserResult } from "."; | |||||
import { cache } from "react"; | import { cache } from "react"; | ||||
export interface UserInputs { | export interface UserInputs { | ||||
username: string; | |||||
firstname: string; | |||||
lastname: string; | |||||
name: string; | |||||
email?: string; | |||||
addAuthIds?: number[]; | |||||
removeAuthIds?: number[]; | |||||
} | |||||
export interface PasswordInputs { | |||||
password: string; | |||||
newPassword: string; | |||||
newPasswordCheck: string; | |||||
} | } | ||||
@@ -19,9 +26,25 @@ export const fetchUserDetails = cache(async (id: number) => { | |||||
}); | }); | ||||
}); | }); | ||||
export const editUser = async (id: number, data: UserInputs) => { | |||||
return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { | |||||
method: "PUT", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}); | |||||
}; | |||||
export const deleteUser = async (id: number) => { | export const deleteUser = async (id: number) => { | ||||
return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { | return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { | ||||
method: "DELETE", | method: "DELETE", | ||||
headers: { "Content-Type": "application/json" }, | headers: { "Content-Type": "application/json" }, | ||||
}); | }); | ||||
}; | |||||
export const changePassword = async (data: any) => { | |||||
return serverFetchWithNoContent(`${BASE_API_URL}/user/change-password`, { | |||||
method: "PATCH", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}); | |||||
}; | }; |
@@ -3,7 +3,6 @@ import { BASE_API_URL } from "@/config/api"; | |||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import "server-only"; | import "server-only"; | ||||
export interface UserResult { | export interface UserResult { | ||||
action: any; | action: any; | ||||
id: number; | id: number; | ||||
@@ -19,6 +18,8 @@ export interface UserResult { | |||||
phone1: string; | phone1: string; | ||||
phone2: string; | phone2: string; | ||||
remarks: string; | remarks: string; | ||||
groupId: number; | |||||
auths: any | |||||
} | } | ||||
// export interface DetailedUser extends UserResult { | // export interface DetailedUser extends UserResult { | ||||
@@ -27,9 +28,10 @@ export interface UserResult { | |||||
// } | // } | ||||
export interface UserDetail { | export interface UserDetail { | ||||
authIds: number[]; | |||||
data: UserResult; | data: UserResult; | ||||
authIds: number[]; | |||||
groupIds: number[]; | groupIds: number[]; | ||||
auths: any[] | |||||
} | } | ||||
export const preloadUser = () => { | export const preloadUser = () => { | ||||
@@ -3,6 +3,16 @@ import { getServerSession } from "next-auth"; | |||||
import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||
import { redirect } from "next/navigation"; | import { redirect } from "next/navigation"; | ||||
export class ServerFetchError extends Error { | |||||
public readonly response: Response | undefined; | |||||
constructor(message?: string, response?: Response) { | |||||
super(message); | |||||
this.response = response; | |||||
Object.setPrototypeOf(this, ServerFetchError.prototype); | |||||
} | |||||
} | |||||
export const serverFetch: typeof fetch = async (input, init) => { | export const serverFetch: typeof fetch = async (input, init) => { | ||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
const session = await getServerSession<any, SessionWithTokens>(authOptions); | const session = await getServerSession<any, SessionWithTokens>(authOptions); | ||||
@@ -17,7 +27,7 @@ export const serverFetch: typeof fetch = async (input, init) => { | |||||
? { | ? { | ||||
Authorization: `Bearer ${accessToken}`, | Authorization: `Bearer ${accessToken}`, | ||||
Accept: | Accept: | ||||
"application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |||||
"application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, multipart/form-data", | |||||
} | } | ||||
: {}), | : {}), | ||||
}, | }, | ||||
@@ -37,7 +47,10 @@ export async function serverFetchJson<T>(...args: FetchParams) { | |||||
signOutUser(); | signOutUser(); | ||||
default: | default: | ||||
console.error(await response.text()); | console.error(await response.text()); | ||||
throw Error("Something went wrong fetching data in server."); | |||||
throw new ServerFetchError( | |||||
"Something went wrong fetching data in server.", | |||||
response, | |||||
); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -58,6 +71,25 @@ export async function serverFetchWithNoContent(...args: FetchParams) { | |||||
} | } | ||||
} | } | ||||
export async function serverFetchString<T>(...args: FetchParams) { | |||||
const response = await serverFetch(...args); | |||||
if (response.ok) { | |||||
return response.text() as T; | |||||
} else { | |||||
switch (response.status) { | |||||
case 401: | |||||
signOutUser(); | |||||
default: | |||||
console.error(await response.text()); | |||||
throw new ServerFetchError( | |||||
"Something went wrong fetching data in server.", | |||||
response, | |||||
); | |||||
} | |||||
} | |||||
} | |||||
export async function serverFetchBlob<T>(...args: FetchParams) { | export async function serverFetchBlob<T>(...args: FetchParams) { | ||||
const response = await serverFetch(...args); | const response = await serverFetch(...args); | ||||
@@ -30,6 +30,12 @@ export const convertDateArrayToString = (dateArray: number[], format: string = O | |||||
return dayjs(dateString).format(format) | return dayjs(dateString).format(format) | ||||
} | } | ||||
} | } | ||||
if (dateArray.length === 3) { | |||||
if (!needTime) { | |||||
const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}` | |||||
return dayjs(dateString).format(format) | |||||
} | |||||
} | |||||
} | } | ||||
const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", { | const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", { | ||||
@@ -5,18 +5,23 @@ import Profile from "./Profile"; | |||||
import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||
import NavigationToggle from "./NavigationToggle"; | import NavigationToggle from "./NavigationToggle"; | ||||
import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
import { authOptions } from "@/config/authConfig"; | |||||
import { getServerSession } from "next-auth"; | |||||
export interface AppBarProps { | export interface AppBarProps { | ||||
avatarImageSrc?: string; | avatarImageSrc?: string; | ||||
profileName: string; | profileName: string; | ||||
} | } | ||||
const AppBar: React.FC<AppBarProps> = ({ avatarImageSrc, profileName }) => { | |||||
const AppBar: React.FC<AppBarProps> = async ({ avatarImageSrc, profileName }) => { | |||||
const session = await getServerSession(authOptions) as any; | |||||
const abilities: string[] = session.abilities | |||||
console.log(abilities) | |||||
return ( | return ( | ||||
<I18nProvider namespaces={["common"]}> | <I18nProvider namespaces={["common"]}> | ||||
<MUIAppBar position="sticky" color="default" elevation={4}> | <MUIAppBar position="sticky" color="default" elevation={4}> | ||||
<Toolbar> | <Toolbar> | ||||
<NavigationToggle /> | |||||
<NavigationToggle abilities={abilities}/> | |||||
<Box | <Box | ||||
sx={{ flexGrow: 1, display: "flex", justifyContent: "flex-end" }} | sx={{ flexGrow: 1, display: "flex", justifyContent: "flex-end" }} | ||||
> | > | ||||
@@ -4,8 +4,18 @@ import MenuIcon from "@mui/icons-material/Menu"; | |||||
import NavigationContent from "../NavigationContent"; | import NavigationContent from "../NavigationContent"; | ||||
import React from "react"; | import React from "react"; | ||||
import Drawer from "@mui/material/Drawer"; | import Drawer from "@mui/material/Drawer"; | ||||
import { Session } from "inspector"; | |||||
import { authOptions } from "@/config/authConfig"; | |||||
import { getServerSession } from "next-auth"; | |||||
export interface SessionWithAbilities extends Session { | |||||
abilities?: string[] | |||||
} | |||||
const NavigationToggle: React.FC = () => { | |||||
interface Props { | |||||
abilities?: string[] | |||||
} | |||||
const NavigationToggle: React.FC<Props> = ({ abilities }) => { | |||||
const [isOpened, setIsOpened] = React.useState(false); | const [isOpened, setIsOpened] = React.useState(false); | ||||
const openNavigation = () => { | const openNavigation = () => { | ||||
@@ -18,7 +28,7 @@ const NavigationToggle: React.FC = () => { | |||||
return ( | return ( | ||||
<> | <> | ||||
<Drawer variant="permanent" sx={{ display: { xs: "none", xl: "block" } }}> | <Drawer variant="permanent" sx={{ display: { xs: "none", xl: "block" } }}> | ||||
<NavigationContent /> | |||||
<NavigationContent abilities={abilities}/> | |||||
</Drawer> | </Drawer> | ||||
<Drawer | <Drawer | ||||
sx={{ display: { xl: "none" } }} | sx={{ display: { xl: "none" } }} | ||||
@@ -28,7 +38,7 @@ const NavigationToggle: React.FC = () => { | |||||
keepMounted: true, | keepMounted: true, | ||||
}} | }} | ||||
> | > | ||||
<NavigationContent /> | |||||
<NavigationContent abilities={abilities}/> | |||||
</Drawer> | </Drawer> | ||||
<IconButton | <IconButton | ||||
sx={{ display: { xl: "none" } }} | sx={{ display: { xl: "none" } }} | ||||
@@ -10,6 +10,7 @@ import Divider from "@mui/material/Divider"; | |||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { signOut } from "next-auth/react"; | import { signOut } from "next-auth/react"; | ||||
import { useRouter } from "next/navigation"; | |||||
type Props = Pick<AppBarProps, "avatarImageSrc" | "profileName">; | type Props = Pick<AppBarProps, "avatarImageSrc" | "profileName">; | ||||
@@ -26,6 +27,7 @@ const Profile: React.FC<Props> = ({ avatarImageSrc, profileName }) => { | |||||
}; | }; | ||||
const { t } = useTranslation("login"); | const { t } = useTranslation("login"); | ||||
const router = useRouter(); | |||||
return ( | return ( | ||||
<> | <> | ||||
@@ -52,6 +54,7 @@ const Profile: React.FC<Props> = ({ avatarImageSrc, profileName }) => { | |||||
{profileName} | {profileName} | ||||
</Typography> | </Typography> | ||||
<Divider /> | <Divider /> | ||||
<MenuItem onClick={() => {router.replace("/settings/changepassword")}}>{t("Change Password")}</MenuItem> | |||||
<MenuItem onClick={() => signOut()}>{t("Sign out")}</MenuItem> | <MenuItem onClick={() => signOut()}>{t("Sign out")}</MenuItem> | ||||
</Menu> | </Menu> | ||||
</> | </> | ||||
@@ -12,6 +12,7 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
"/home": "User Workspace", | "/home": "User Workspace", | ||||
"/projects": "Projects", | "/projects": "Projects", | ||||
"/projects/create": "Create Project", | "/projects/create": "Create Project", | ||||
"/projects/edit": "Edit Project", | |||||
"/tasks": "Task Template", | "/tasks": "Task Template", | ||||
"/tasks/create": "Create Task Template", | "/tasks/create": "Create Task Template", | ||||
"/staffReimbursement": "Staff Reimbursement", | "/staffReimbursement": "Staff Reimbursement", | ||||
@@ -28,7 +29,8 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
"/settings/position": "Position", | "/settings/position": "Position", | ||||
"/settings/position/new": "Create Position", | "/settings/position/new": "Create Position", | ||||
"/settings/salarys": "Salary", | "/settings/salarys": "Salary", | ||||
"/analytics/EX02ProjectCashFlowReport": "EX02 - Project Cash Flow Report", | |||||
"/analytics/ProjectCashFlowReport": "Project Cash Flow Report", | |||||
"/settings/holiday": "Holiday", | |||||
}; | }; | ||||
const Breadcrumb = () => { | const Breadcrumb = () => { | ||||
@@ -0,0 +1,107 @@ | |||||
"use client"; | |||||
import { PasswordInputs, changePassword } from "@/app/api/user/actions"; | |||||
import { Grid } from "@mui/material"; | |||||
import { useRouter } from "next/navigation"; | |||||
import { useCallback, useState } from "react"; | |||||
import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; | |||||
import { Check, Close, Error } from "@mui/icons-material"; | |||||
import ChagnePasswordForm from "./ChangePasswordForm"; | |||||
import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||||
// interface Props { | |||||
// // auth?: auth[] | |||||
// // users?: UserResult[] | |||||
// } | |||||
const ChangePassword: React.FC = () => { | |||||
const formProps = useForm<PasswordInputs>(); | |||||
const [serverError, setServerError] = useState(""); | |||||
const router = useRouter(); | |||||
// const [tabIndex, setTabIndex] = useState(0); | |||||
const { t } = useTranslation(); | |||||
const onSubmit = useCallback<SubmitHandler<PasswordInputs>>( | |||||
async (data) => { | |||||
try { | |||||
let haveError = false; | |||||
// Minimum eight characters, at least one uppercase letter, one lowercase letter, one number and one special character: | |||||
let regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/ | |||||
if (data.newPassword.length < 8 || data.newPassword.length > 20) { | |||||
haveError = true | |||||
formProps.setError("newPassword", { message: "The password requires 8-20 characters", type: "required" }) | |||||
} | |||||
if (!regex.test(data.newPassword)) { | |||||
haveError = true | |||||
formProps.setError("newPassword", { message: "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.", type: "required" }) | |||||
} | |||||
if (data.password == data.newPassword) { | |||||
haveError = true | |||||
formProps.setError("newPassword", { message: "The new password cannot be the same as the old password", type: "required" }) | |||||
} | |||||
if (data.newPassword != data.newPasswordCheck) { | |||||
haveError = true | |||||
formProps.setError("newPassword", { message: "The new password has to be the same as the new password", type: "required" }) | |||||
formProps.setError("newPasswordCheck", { message: "The new password has to be the same as the new password", type: "required" }) | |||||
} | |||||
if (haveError) { | |||||
return | |||||
} | |||||
const postData = { | |||||
password: data.password, | |||||
newPassword: data.newPassword | |||||
} | |||||
// await changePassword(postData) | |||||
// router.replace("/home") | |||||
} catch (e) { | |||||
console.log(e) | |||||
setServerError(t("An error has occurred. Please try again later.")); | |||||
} | |||||
}, | |||||
[router] | |||||
); | |||||
const handleCancel = () => { | |||||
router.push(`/home`); | |||||
}; | |||||
const onSubmitError = useCallback<SubmitErrorHandler<PasswordInputs>>( | |||||
(errors) => { | |||||
console.log(errors); | |||||
}, | |||||
[] | |||||
); | |||||
return ( | |||||
<FormProvider {...formProps}> | |||||
<Stack | |||||
spacing={2} | |||||
component="form" | |||||
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||||
> | |||||
<ChagnePasswordForm /> | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
<Button | |||||
variant="outlined" | |||||
startIcon={<Close />} | |||||
onClick={handleCancel} | |||||
> | |||||
{t("Cancel")} | |||||
</Button> | |||||
<Button | |||||
variant="contained" | |||||
startIcon={<Check />} | |||||
type="submit" | |||||
// disabled={Boolean(formProps.watch("isGridEditing"))} | |||||
> | |||||
{t("Confirm")} | |||||
</Button> | |||||
</Stack> | |||||
</Stack> | |||||
</FormProvider> | |||||
); | |||||
}; | |||||
export default ChangePassword; |
@@ -0,0 +1,144 @@ | |||||
"use client"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import Box from "@mui/material/Box"; | |||||
import Card from "@mui/material/Card"; | |||||
import CardContent from "@mui/material/CardContent"; | |||||
import Grid from "@mui/material/Grid"; | |||||
import TextField from "@mui/material/TextField"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import { useFormContext } from "react-hook-form"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { useCallback, useState } from "react"; | |||||
import { PasswordInputs } from "@/app/api/user/actions"; | |||||
import { Visibility, VisibilityOff } from "@mui/icons-material"; | |||||
import { IconButton, InputAdornment } from "@mui/material"; | |||||
const ChagnePasswordForm: React.FC = () => { | |||||
const { t } = useTranslation(); | |||||
const [showNewPassword, setShowNewPassword] = useState(false); | |||||
const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword); | |||||
const handleMouseDownNewPassword = () => setShowNewPassword(!showNewPassword); | |||||
const [showPassword, setShowPassword] = useState(false); | |||||
const handleClickShowPassword = () => setShowPassword(!showPassword); | |||||
const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||||
const { | |||||
register, | |||||
formState: { errors, defaultValues }, | |||||
control, | |||||
reset, | |||||
resetField, | |||||
setValue, | |||||
} = useFormContext<PasswordInputs>(); | |||||
// const resetGroup = useCallback(() => { | |||||
// console.log(defaultValues); | |||||
// if (defaultValues !== undefined) { | |||||
// resetField("description"); | |||||
// } | |||||
// }, [defaultValues]); | |||||
return ( | |||||
<Card sx={{ display: "block" }}> | |||||
<CardContent component={Stack} spacing={4}> | |||||
<Box> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Group Info")} | |||||
</Typography> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs={6}> | |||||
<TextField | |||||
label={t("Input Old Password")} | |||||
fullWidth | |||||
type={showPassword ? "text" : "password"} | |||||
InputProps={{ | |||||
endAdornment: ( | |||||
<InputAdornment position="end"> | |||||
<IconButton | |||||
aria-label="toggle password visibility" | |||||
onClick={handleClickShowPassword} | |||||
onMouseDown={handleMouseDownPassword} | |||||
> | |||||
{showPassword ? <Visibility /> : <VisibilityOff />} | |||||
</IconButton> | |||||
</InputAdornment> | |||||
) | |||||
}} | |||||
{...register("password", { | |||||
required: true, | |||||
})} | |||||
error={Boolean(errors.password)} | |||||
helperText={ | |||||
Boolean(errors.password) && | |||||
(errors.password?.message | |||||
? t(errors.password.message) | |||||
: t("Please input correct password")) | |||||
} | |||||
/> | |||||
</Grid> | |||||
<Grid item xs={6} /> | |||||
<Grid item xs={6}> | |||||
<TextField | |||||
label={t("Input New Password")} | |||||
fullWidth | |||||
type={showNewPassword ? "text" : "password"} | |||||
InputProps={{ | |||||
endAdornment: ( | |||||
<InputAdornment position="end"> | |||||
<IconButton | |||||
aria-label="toggle password visibility" | |||||
onClick={handleClickShowNewPassword} | |||||
onMouseDown={handleMouseDownNewPassword} | |||||
> | |||||
{showNewPassword ? <Visibility /> : <VisibilityOff />} | |||||
</IconButton> | |||||
</InputAdornment> | |||||
) | |||||
}} | |||||
{...register("newPassword")} | |||||
error={Boolean(errors.newPassword)} | |||||
helperText={ | |||||
Boolean(errors.newPassword) && | |||||
(errors.newPassword?.message | |||||
? t(errors.newPassword.message) | |||||
: t("Please input correct newPassword")) | |||||
} | |||||
/> | |||||
</Grid> | |||||
<Grid item xs={6}> | |||||
<TextField | |||||
label={t("Input New Password Again")} | |||||
fullWidth | |||||
type={showNewPassword ? "text" : "password"} | |||||
InputProps={{ | |||||
endAdornment: ( | |||||
<InputAdornment position="end"> | |||||
<IconButton | |||||
aria-label="toggle password visibility" | |||||
onClick={handleClickShowNewPassword} | |||||
onMouseDown={handleMouseDownNewPassword} | |||||
> | |||||
{showNewPassword ? <Visibility /> : <VisibilityOff />} | |||||
</IconButton> | |||||
</InputAdornment> | |||||
) | |||||
}} | |||||
{...register("newPasswordCheck")} | |||||
error={Boolean(errors.newPassword)} | |||||
helperText={ | |||||
Boolean(errors.newPassword) && | |||||
(errors.newPassword?.message | |||||
? t(errors.newPassword.message) | |||||
: t("Please input correct newPassword")) | |||||
} | |||||
/> | |||||
</Grid> | |||||
</Grid> | |||||
</Box> | |||||
</CardContent> | |||||
</Card> | |||||
); | |||||
}; | |||||
export default ChagnePasswordForm; |
@@ -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 ChangePasswordLoading: React.FC = () => { | |||||
return ( | |||||
<> | |||||
<Card> | |||||
<CardContent> | |||||
<Stack spacing={2}> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton | |||||
variant="rounded" | |||||
height={50} | |||||
width={100} | |||||
sx={{ alignSelf: "flex-end" }} | |||||
/> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
<Card>Change Password | |||||
<CardContent> | |||||
<Stack spacing={2}> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
</> | |||||
); | |||||
}; | |||||
export default ChangePasswordLoading; |
@@ -0,0 +1,20 @@ | |||||
import React from "react"; | |||||
import ChangePasswordLoading from "./ChangePasswordLoading"; | |||||
import ChangePassword from "./ChangePassword"; | |||||
interface SubComponents { | |||||
Loading: typeof ChangePasswordLoading; | |||||
} | |||||
const ChangePasswordWrapper: React.FC & SubComponents = async () => { | |||||
// const records = await fetchAuth() | |||||
// const users = await fetchUser() | |||||
// console.log(users) | |||||
// const auth = records.records as auth[] | |||||
return <ChangePassword />; | |||||
}; | |||||
ChangePasswordWrapper.Loading = ChangePasswordLoading; | |||||
export default ChangePasswordWrapper; |
@@ -0,0 +1 @@ | |||||
export { default } from "./ChangePasswordWrapper"; |
@@ -1 +0,0 @@ | |||||
export { default } from "./ClaimDetailWrapper"; |
@@ -21,7 +21,7 @@ export interface Props { | |||||
projectCombo: ProjectCombo[] | projectCombo: ProjectCombo[] | ||||
} | } | ||||
const ClaimDetail: React.FC<Props> = ({ projectCombo }) => { | |||||
const ClaimSave: React.FC<Props> = ({ projectCombo }) => { | |||||
const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
const router = useRouter(); | const router = useRouter(); | ||||
@@ -74,15 +74,15 @@ const ClaimDetail: React.FC<Props> = ({ projectCombo }) => { | |||||
const buttonName = (event?.nativeEvent as any).submitter.name | const buttonName = (event?.nativeEvent as any).submitter.name | ||||
const formData = new FormData() | const formData = new FormData() | ||||
formData.append("expenseType", data.expenseType) | formData.append("expenseType", data.expenseType) | ||||
data.addClaimDetails.forEach((claimDetail) => { | |||||
console.log(claimDetail) | |||||
formData.append("addClaimDetailIds", JSON.stringify(claimDetail.id)) | |||||
formData.append("addClaimDetailInvoiceDates", convertDateToString(claimDetail.invoiceDate, "YYYY-MM-DD")) | |||||
formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project)) | |||||
formData.append("addClaimDetailDescriptions", claimDetail.description) | |||||
formData.append("addClaimDetailAmounts", JSON.stringify(claimDetail.amount)) | |||||
formData.append("addClaimDetailNewSupportingDocuments", claimDetail.newSupportingDocument) | |||||
formData.append("addClaimDetailOldSupportingDocumentIds", JSON.stringify(claimDetail?.oldSupportingDocument?.id ?? -1)) | |||||
data.addClaimDetails.forEach((ClaimSave) => { | |||||
console.log(ClaimSave) | |||||
formData.append("addClaimDetailIds", JSON.stringify(ClaimSave.id)) | |||||
formData.append("addClaimDetailInvoiceDates", convertDateToString(ClaimSave.invoiceDate, "YYYY-MM-DD")) | |||||
formData.append("addClaimDetailProjectIds", JSON.stringify(ClaimSave.project)) | |||||
formData.append("addClaimDetailDescriptions", ClaimSave.description) | |||||
formData.append("addClaimDetailAmounts", JSON.stringify(ClaimSave.amount)) | |||||
formData.append("addClaimDetailNewSupportingDocuments", ClaimSave.newSupportingDocument) | |||||
formData.append("addClaimDetailOldSupportingDocumentIds", JSON.stringify(ClaimSave?.oldSupportingDocument?.id ?? -1)) | |||||
}) | }) | ||||
// for (let i = 0; i < data.addClaimDetails.length; i++) { | // for (let i = 0; i < data.addClaimDetails.length; i++) { | ||||
// const updatedData = { | // const updatedData = { | ||||
@@ -155,4 +155,4 @@ const ClaimDetail: React.FC<Props> = ({ projectCombo }) => { | |||||
); | ); | ||||
}; | }; | ||||
export default ClaimDetail; | |||||
export default ClaimSave; |
@@ -1,6 +1,6 @@ | |||||
import React from "react"; | import React from "react"; | ||||
import ClaimDetail from "./ClaimDetail"; | |||||
import ClaimSave from "./ClaimSave"; | |||||
import { fetchProjectCombo } from "@/app/api/claims"; | import { fetchProjectCombo } from "@/app/api/claims"; | ||||
// import TaskSetup from "./TaskSetup"; | // import TaskSetup from "./TaskSetup"; | ||||
// import StaffAllocation from "./StaffAllocation"; | // import StaffAllocation from "./StaffAllocation"; | ||||
@@ -13,7 +13,7 @@ const ClaimDetailWrapper: React.FC = async () => { | |||||
]); | ]); | ||||
return ( | return ( | ||||
<ClaimDetail projectCombo={projectCombo}/> | |||||
<ClaimSave projectCombo={projectCombo}/> | |||||
); | ); | ||||
}; | }; | ||||
@@ -0,0 +1 @@ | |||||
export { default } from "./ClaimSaveWrapper"; |
@@ -0,0 +1,227 @@ | |||||
"use client"; | |||||
import { HolidaysList, HolidaysResult } from "@/app/api/holidays"; | |||||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Grid, Stack } from '@mui/material/'; | |||||
import { useTranslation } from "react-i18next"; | |||||
import FullCalendar from '@fullcalendar/react' | |||||
import dayGridPlugin from '@fullcalendar/daygrid' // a plugin! | |||||
import interactionPlugin from "@fullcalendar/interaction" // needed for dayClick | |||||
import listPlugin from '@fullcalendar/list'; | |||||
import Holidays from "date-holidays"; | |||||
import CompanyHolidayDialog from "./CompanyHolidayDialog"; | |||||
import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form"; | |||||
import { EventBusy } from "@mui/icons-material"; | |||||
import { deleteCompanyHoliday, saveCompanyHoliday } from "@/app/api/holidays/actions"; | |||||
import { useRouter } from "next/navigation"; | |||||
import { deleteDialog, submitDialog } from "../Swal/CustomAlerts"; | |||||
interface Props { | |||||
holidays: HolidaysList[]; | |||||
} | |||||
const CompanyHoliday: React.FC<Props> = ({ holidays }) => { | |||||
const { t } = useTranslation("holidays"); | |||||
const router = useRouter(); | |||||
const formValues = useFormContext(); | |||||
const [serverError, setServerError] = useState(""); | |||||
const hd = new Holidays('HK') | |||||
console.log(holidays) | |||||
const [companyHolidays, setCompanyHolidays] = useState<HolidaysList[]>([]) | |||||
const [dateContent, setDateContent] = useState<{ date: string }>({date: ''}) | |||||
const [open, setOpen] = useState(false); | |||||
const [isEdit, setIsEdit] = useState(false); | |||||
const [editable, setEditable] = useState(true); | |||||
const handleClose = () => { | |||||
setOpen(false); | |||||
setEditable(true) | |||||
setIsEdit(false) | |||||
formProps.setValue("name", "") | |||||
formProps.setValue("id", null) | |||||
}; | |||||
const getPublicHolidaysList = () => { | |||||
const currentYear = new Date().getFullYear() | |||||
const currentYearHolidays = hd.getHolidays(currentYear) | |||||
const nextYearHolidays = hd.getHolidays(currentYear + 1) | |||||
const events_cyhd = currentYearHolidays.map(ele => { | |||||
const tempDay = new Date(ele.date) | |||||
const tempYear = tempDay.getFullYear() | |||||
const tempMonth = tempDay.getMonth() + 1 < 10 ? `0${ tempDay.getMonth() + 1}` : tempDay.getMonth() + 1 | |||||
const tempDate = tempDay.getDate() < 10 ? `0${tempDay.getDate()}` : tempDay.getDate() | |||||
let tempName = "" | |||||
switch (ele.name) { | |||||
case "复活节": | |||||
tempName = "復活節" | |||||
break | |||||
case "劳动节": | |||||
tempName = "勞動節" | |||||
break | |||||
case "端午节": | |||||
tempName = "端午節" | |||||
break | |||||
case "重阳节": | |||||
tempName = "重陽節" | |||||
break | |||||
case "圣诞节后的第一个工作日": | |||||
tempName = "聖誕節後的第一个工作日" | |||||
break | |||||
default: | |||||
tempName = ele.name | |||||
break | |||||
} | |||||
return {date: `${tempYear}-${tempMonth}-${tempDate}`, title: tempName, extendedProps: {calendar: 'holiday'}} | |||||
}) | |||||
const events_nyhd = nextYearHolidays.map(ele => { | |||||
const tempDay = new Date(ele.date) | |||||
const tempYear = tempDay.getFullYear() | |||||
const tempMonth = tempDay.getMonth() + 1 < 10 ? `0${ tempDay.getMonth() + 1}` : tempDay.getMonth() + 1 | |||||
const tempDate = tempDay.getDate() < 10 ? `0${tempDay.getDate()}` : tempDay.getDate() | |||||
let tempName = "" | |||||
switch (ele.name) { | |||||
case "复活节": | |||||
tempName = "復活節" | |||||
break | |||||
case "劳动节": | |||||
tempName = "勞動節" | |||||
break | |||||
case "端午节": | |||||
tempName = "端午節" | |||||
break | |||||
case "重阳节": | |||||
tempName = "重陽節" | |||||
break | |||||
case "圣诞节后的第一个工作日": | |||||
tempName = "聖誕節後的第一个工作日" | |||||
break | |||||
default: | |||||
tempName = ele.name | |||||
break | |||||
} | |||||
return {date: `${tempYear}-${tempMonth}-${tempDate}`, title: tempName, extendedProps: {calendar: 'holiday'}} | |||||
}) | |||||
setCompanyHolidays([...events_cyhd, ...events_nyhd, ...holidays] as HolidaysList[]) | |||||
} | |||||
useEffect(()=>{ | |||||
getPublicHolidaysList() | |||||
},[]) | |||||
useEffect(()=>{ | |||||
},[holidays]) | |||||
const handleDateClick = (event:any) => { | |||||
// console.log(event.dateStr) | |||||
setDateContent({date: event.dateStr}) | |||||
setOpen(true); | |||||
} | |||||
const handleEventClick = (event:any) => { | |||||
// event.event.id: if id !== "", holiday is created by company | |||||
console.log(event.event.id) | |||||
if (event.event.id === null || event.event.id === ""){ | |||||
setEditable(false) | |||||
} | |||||
formProps.setValue("name", event.event.title) | |||||
formProps.setValue("id", event.event.id) | |||||
setDateContent({date: event.event.startStr}) | |||||
setOpen(true); | |||||
setIsEdit(true); | |||||
} | |||||
const onSubmit = useCallback<SubmitHandler<any>>( | |||||
async (data) => { | |||||
try { | |||||
// console.log(data); | |||||
setServerError(""); | |||||
submitDialog(async () => { | |||||
await saveCompanyHoliday(data) | |||||
window.location.reload() | |||||
setOpen(false); | |||||
setIsEdit(false); | |||||
}, t) | |||||
} catch (e) { | |||||
console.log(e); | |||||
setServerError(t("An error has occurred. Please try again later.")); | |||||
} | |||||
}, | |||||
[t, router], | |||||
); | |||||
const handleDelete = async (event:any) => { | |||||
try { | |||||
setServerError(""); | |||||
deleteDialog(async () => { | |||||
await deleteCompanyHoliday(parseInt(formProps.getValues("id"))) | |||||
window.location.reload() | |||||
setOpen(false); | |||||
setIsEdit(false); | |||||
}, t); | |||||
} catch (e) { | |||||
console.log(e); | |||||
setServerError(t("An error has occurred. Please try again later.")); | |||||
} | |||||
} | |||||
const onSubmitError = useCallback<SubmitErrorHandler<any>>( | |||||
(errors) => { | |||||
console.log(errors) | |||||
}, | |||||
[], | |||||
); | |||||
const formProps = useForm<any>({ | |||||
defaultValues: { | |||||
id: null, | |||||
name: "" | |||||
}, | |||||
}); | |||||
return ( | |||||
<> | |||||
<FormProvider {...formProps}> | |||||
<FullCalendar | |||||
plugins={[ dayGridPlugin, interactionPlugin, listPlugin ]} | |||||
initialView="dayGridMonth" | |||||
events={companyHolidays} | |||||
eventColor='#ff0000' | |||||
dateClick={handleDateClick} | |||||
eventClick={handleEventClick} | |||||
headerToolbar={{ | |||||
start: "today prev next", | |||||
end: "dayGridMonth listMonth" | |||||
}} | |||||
buttonText={{ | |||||
month: t("Calender View"), | |||||
list: t("List View"), | |||||
today: t("Today") | |||||
}} | |||||
/> | |||||
<CompanyHolidayDialog | |||||
open={open} | |||||
onClose={handleClose} | |||||
title={!editable ? "Bank Holiday" : isEdit ? "Edit Holiday" : "Create Holiday"} | |||||
content={dateContent} | |||||
actions={ | |||||
<Stack direction="row" justifyContent="flex-end" gap={1} component="form" onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}> | |||||
<Button onClick={handleClose}>Close</Button> | |||||
{isEdit && <Button disabled={!editable} onClick={handleDelete}>Delete</Button>} | |||||
<Button disabled={!editable} type="submit">Submit</Button> | |||||
</Stack> | |||||
} | |||||
editable={editable} | |||||
/> | |||||
</FormProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default CompanyHoliday; |
@@ -0,0 +1,87 @@ | |||||
import React, { useState, useEffect } from 'react'; | |||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Grid, FormControl } from '@mui/material/'; | |||||
import { useForm, useFormContext } from 'react-hook-form'; | |||||
import { useTranslation } from 'react-i18next'; | |||||
import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; | |||||
import dayjs from 'dayjs'; | |||||
import { INPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; | |||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; | |||||
interface CompanyHolidayDialogProps { | |||||
open: boolean; | |||||
onClose: () => void; | |||||
title: string; | |||||
actions: React.ReactNode; | |||||
content: Content; | |||||
editable: Boolean; | |||||
} | |||||
interface Content { | |||||
date: string | |||||
} | |||||
const CompanyHolidayDialog: React.FC<CompanyHolidayDialogProps> = ({ open, onClose, title, actions, content, editable }) => { | |||||
const { | |||||
t, | |||||
i18n: { language }, | |||||
} = useTranslation(); | |||||
const { | |||||
register, | |||||
formState: { errors }, | |||||
setValue, | |||||
} = useFormContext<any>(); | |||||
useEffect(() => { | |||||
setValue("date", content.date); | |||||
}, [content]) | |||||
console.log(editable) | |||||
return ( | |||||
<LocalizationProvider | |||||
dateAdapter={AdapterDayjs} | |||||
adapterLocale={`${language}-hk`} | |||||
> | |||||
<Dialog open={open} onClose={onClose}> | |||||
<DialogTitle>{title}</DialogTitle> | |||||
<DialogContent> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs={12}> | |||||
<TextField | |||||
disabled={!editable} | |||||
label={t("Description")} | |||||
fullWidth | |||||
{...register("name", { | |||||
required: "Description required!", | |||||
})} | |||||
error={Boolean(errors.name)} | |||||
/> | |||||
</Grid> | |||||
<Grid item xs={12}> | |||||
<FormControl fullWidth> | |||||
<DatePicker | |||||
disabled={!editable} | |||||
label={t("Company Holiday")} | |||||
value={dayjs(content.date)} | |||||
onChange={(date) => { | |||||
if (!date) return; | |||||
setValue("date", date.format(INPUT_DATE_FORMAT)); | |||||
}} | |||||
slotProps={{ | |||||
textField: { | |||||
helperText: 'MM/DD/YYYY', | |||||
}, | |||||
}} | |||||
/> | |||||
</FormControl> | |||||
</Grid> | |||||
</Grid> | |||||
</DialogContent> | |||||
<DialogActions>{actions}</DialogActions> | |||||
</Dialog> | |||||
</LocalizationProvider> | |||||
); | |||||
}; | |||||
export default CompanyHolidayDialog; |
@@ -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 CompanyHolidayLoading: React.FC = () => { | |||||
return ( | |||||
<> | |||||
<Card> | |||||
<CardContent> | |||||
<Stack spacing={2}> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton | |||||
variant="rounded" | |||||
height={50} | |||||
width={100} | |||||
sx={{ alignSelf: "flex-end" }} | |||||
/> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
<Card> | |||||
<CardContent> | |||||
<Stack spacing={2}> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
</> | |||||
); | |||||
}; | |||||
export default CompanyHolidayLoading; |
@@ -0,0 +1,34 @@ | |||||
// import { fetchCompanyCategories, fetchCompanys } from "@/app/api/companys"; | |||||
import React, { useState, } from "react"; | |||||
import CompanyHoliday from "./CompanyHoliday"; | |||||
import CompanyHolidayLoading from "./CompanyHolidayLoading"; | |||||
import { fetchCompanys } from "@/app/api/companys"; | |||||
import Holidays from "date-holidays"; | |||||
import { HolidaysResult, fetchHolidays, HolidaysList } from "@/app/api/holidays"; | |||||
import { convertDateArrayToString } from "@/app/utils/formatUtil"; | |||||
interface SubComponents { | |||||
Loading: typeof CompanyHolidayLoading; | |||||
} | |||||
const CompanyHolidayWrapper: React.FC & SubComponents = async () => { | |||||
// const Companys = await fetchCompanys(); | |||||
const companyHolidays: HolidaysResult[] = await fetchHolidays() | |||||
// console.log(companyHolidays) | |||||
const convertedHolidays = companyHolidays.map((holiday) => { | |||||
return { | |||||
id: holiday.id.toString(), | |||||
title: holiday.name, | |||||
date: convertDateArrayToString(holiday.date, "YYYY-MM-DD", false) | |||||
} | |||||
}) | |||||
return <CompanyHoliday holidays={convertedHolidays as HolidaysList[]} />; | |||||
}; | |||||
CompanyHolidayWrapper.Loading = CompanyHolidayLoading; | |||||
export default CompanyHolidayWrapper; |
@@ -0,0 +1 @@ | |||||
export { default } from "./CompanyHolidayWrapper"; |
@@ -22,18 +22,23 @@ import { Error } from "@mui/icons-material"; | |||||
import { ProjectCategory } from "@/app/api/projects"; | import { ProjectCategory } from "@/app/api/projects"; | ||||
import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||
import DepartmentDetails from "./DepartmentDetails"; | import DepartmentDetails from "./DepartmentDetails"; | ||||
import { DepartmentResult } from "@/app/api/departments"; | |||||
interface Props { | |||||
isEdit: Boolean; | |||||
department?: CreateDepartmentInputs; | |||||
} | |||||
const CreateDepartment: React.FC = ({ | |||||
// allTasks, | |||||
// projectCategories, | |||||
// taskTemplates, | |||||
// teamLeads, | |||||
const CreateDepartment: React.FC<Props> = ({ | |||||
isEdit, | |||||
department, | |||||
}) => { | }) => { | ||||
const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
const router = useRouter(); | const router = useRouter(); | ||||
console.log(department) | |||||
const handleCancel = () => { | const handleCancel = () => { | ||||
router.back(); | router.back(); | ||||
}; | }; | ||||
@@ -62,9 +67,10 @@ const CreateDepartment: React.FC = ({ | |||||
const formProps = useForm<CreateDepartmentInputs>({ | const formProps = useForm<CreateDepartmentInputs>({ | ||||
defaultValues: { | defaultValues: { | ||||
departmentCode: "", | |||||
departmentName: "", | |||||
description: "", | |||||
id: department?.id, | |||||
code: department?.code, | |||||
name: department?.name, | |||||
description: department?.description, | |||||
}, | }, | ||||
}); | }); | ||||
@@ -1,18 +1,24 @@ | |||||
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | ||||
import CreateDepartment from "./CreateDepartment"; | import CreateDepartment from "./CreateDepartment"; | ||||
import { fetchTeamLeads } from "@/app/api/staff"; | import { fetchTeamLeads } from "@/app/api/staff"; | ||||
import { DepartmentResult, fetchDepartmentDetails } from "@/app/api/departments"; | |||||
const CreateDepartmentWrapper: React.FC = async () => { | |||||
// const [tasks, taskTemplates, DepartmentCategories, teamLeads] = | |||||
// await Promise.all([ | |||||
// fetchAllTasks(), | |||||
// fetchTaskTemplates(), | |||||
// fetchDepartmentCategories(), | |||||
// fetchTeamLeads(), | |||||
// ]); | |||||
type CreateDepartmentProps = { isEdit: false }; | |||||
interface EditDepartmentProps { | |||||
isEdit: true; | |||||
departmentId?: string; | |||||
} | |||||
type Props = CreateDepartmentProps | EditDepartmentProps; | |||||
const CreateDepartmentWrapper: React.FC<Props> = async (props) => { | |||||
const departmentInfo = props.isEdit | |||||
? await fetchDepartmentDetails(props.departmentId!) | |||||
: undefined; | |||||
return ( | return ( | ||||
<CreateDepartment | |||||
<CreateDepartment isEdit department={departmentInfo} | |||||
/> | /> | ||||
); | ); | ||||
}; | }; | ||||
@@ -39,20 +39,20 @@ const DepartmentDetails: React.FC = ({ | |||||
<TextField | <TextField | ||||
label={t("Department Code")} | label={t("Department Code")} | ||||
fullWidth | fullWidth | ||||
{...register("departmentCode", { | |||||
{...register("code", { | |||||
required: "Department code required!", | required: "Department code required!", | ||||
})} | })} | ||||
error={Boolean(errors.departmentCode)} | |||||
error={Boolean(errors.code)} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("Department Name")} | label={t("Department Name")} | ||||
fullWidth | fullWidth | ||||
{...register("departmentName", { | |||||
{...register("name", { | |||||
required: "Department name required!", | required: "Department name required!", | ||||
})} | })} | ||||
error={Boolean(errors.departmentName)} | |||||
error={Boolean(errors.name)} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
@@ -0,0 +1,208 @@ | |||||
"use client"; | |||||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
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 { CreateGroupInputs, auth } from "@/app/api/group/actions"; | |||||
import SearchResults, { Column } from "../SearchResults"; | |||||
import { Add, Clear, Remove, Search } from "@mui/icons-material"; | |||||
export interface Props { | |||||
auth: auth[]; | |||||
} | |||||
const AuthorityAllocation: React.FC<Props> = ({ auth }) => { | |||||
const { t } = useTranslation(); | |||||
const { | |||||
setValue, | |||||
getValues, | |||||
formState: { defaultValues }, | |||||
reset, | |||||
resetField, | |||||
} = useFormContext<CreateGroupInputs>(); | |||||
const initialAuths = auth.map((a) => ({ ...a })).sort((a, b) => a.id - b.id); | |||||
const [filteredAuths, setFilteredAuths] = useState(initialAuths); | |||||
const [selectedAuths, setSelectedAuths] = useState<typeof filteredAuths>( | |||||
() => { | |||||
return filteredAuths.filter( | |||||
(s) => getValues("addAuthIds")?.includes(s.id) | |||||
); | |||||
} | |||||
); | |||||
// 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)); | |||||
}, []); | |||||
const clearAuth = useCallback(() => { | |||||
if (defaultValues !== undefined) { | |||||
resetField("addAuthIds"); | |||||
setSelectedAuths( | |||||
initialAuths.filter((s) => defaultValues.addAuthIds?.includes(s.id)) | |||||
); | |||||
} | |||||
}, [defaultValues]); | |||||
// Sync with form | |||||
useEffect(() => { | |||||
setValue( | |||||
"addAuthIds", | |||||
selectedAuths.map((a) => a.id) | |||||
); | |||||
}, [selectedAuths, setValue]); | |||||
const AuthPoolColumns = useMemo<Column<auth>[]>( | |||||
() => [ | |||||
{ | |||||
label: t("Add"), | |||||
name: "id", | |||||
onClick: addAuth, | |||||
buttonIcon: <Add />, | |||||
}, | |||||
{ label: t("authority"), name: "authority" }, | |||||
{ label: t("Auth Name"), name: "name" }, | |||||
// { label: t("Current Position"), name: "currentPosition" }, | |||||
], | |||||
[addAuth, t] | |||||
); | |||||
const allocatedAuthColumns = useMemo<Column<auth>[]>( | |||||
() => [ | |||||
{ | |||||
label: t("Remove"), | |||||
name: "id", | |||||
onClick: removeAuth, | |||||
buttonIcon: <Remove color="warning"/>, | |||||
}, | |||||
{ 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<HTMLInputElement> | |||||
>((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)) | |||||
// }) | |||||
// ); | |||||
}, [auth, query]); | |||||
const resetAuth = React.useCallback(() => { | |||||
clearQueryInput(); | |||||
clearAuth(); | |||||
}, [clearQueryInput, clearAuth]); | |||||
const formProps = useForm({}); | |||||
// Tab related | |||||
const [tabIndex, setTabIndex] = React.useState(0); | |||||
const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
(_e, newValue) => { | |||||
setTabIndex(newValue); | |||||
}, | |||||
[] | |||||
); | |||||
return ( | |||||
<> | |||||
<FormProvider {...formProps}> | |||||
<Card sx={{ display: "block" }}> | |||||
<CardContent | |||||
sx={{ display: "flex", flexDirection: "column", gap: 1 }} | |||||
> | |||||
<Stack gap={2}> | |||||
<Typography variant="overline" display="block"> | |||||
{t("Authority")} | |||||
</Typography> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs={6} display="flex" alignItems="center"> | |||||
<Search sx={{ marginInlineEnd: 1 }} /> | |||||
<TextField | |||||
variant="standard" | |||||
fullWidth | |||||
onChange={onQueryInputChange} | |||||
value={query} | |||||
placeholder={t("Search by staff ID, name or position.")} | |||||
InputProps={{ | |||||
endAdornment: query && ( | |||||
<InputAdornment position="end"> | |||||
<IconButton onClick={clearQueryInput}> | |||||
<Clear /> | |||||
</IconButton> | |||||
</InputAdornment> | |||||
), | |||||
}} | |||||
/> | |||||
</Grid> | |||||
</Grid> | |||||
<Tabs value={tabIndex} onChange={handleTabChange}> | |||||
<Tab label={t("Authority Pool")} /> | |||||
<Tab | |||||
label={`${t("Allocated Authority")} (${selectedAuths.length})`} | |||||
/> | |||||
</Tabs> | |||||
<Box sx={{ marginInline: -3 }}> | |||||
{tabIndex === 0 && ( | |||||
<SearchResults | |||||
noWrapper | |||||
items={differenceBy(filteredAuths, selectedAuths, "id")} | |||||
columns={AuthPoolColumns} | |||||
/> | |||||
)} | |||||
{tabIndex === 1 && ( | |||||
<SearchResults | |||||
noWrapper | |||||
items={selectedAuths} | |||||
columns={allocatedAuthColumns} | |||||
/> | |||||
)} | |||||
</Box> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
</FormProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default AuthorityAllocation; |
@@ -0,0 +1,130 @@ | |||||
"use client"; | |||||
import { CreateGroupInputs, auth, saveGroup } from "@/app/api/group/actions"; | |||||
import { useRouter } from "next/navigation"; | |||||
import { useCallback, useState } from "react"; | |||||
import { FieldErrors, FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; | |||||
import { Check, Close, Error } from "@mui/icons-material"; | |||||
import GroupInfo from "./GroupInfo"; | |||||
import AuthorityAllocation from "./AuthorityAllocation"; | |||||
import UserAllocation from "./UserAllocation"; | |||||
import { UserResult } from "@/app/api/user"; | |||||
interface Props { | |||||
auth?: auth[] | |||||
users?: UserResult[] | |||||
} | |||||
const CreateGroup: React.FC<Props> = ({ auth, users }) => { | |||||
const formProps = useForm<CreateGroupInputs>(); | |||||
const [serverError, setServerError] = useState(""); | |||||
const router = useRouter(); | |||||
const [tabIndex, setTabIndex] = useState(0); | |||||
const { t } = useTranslation(); | |||||
const errors = formProps.formState.errors; | |||||
const onSubmit = useCallback<SubmitHandler<CreateGroupInputs>>( | |||||
async (data) => { | |||||
try { | |||||
console.log(data); | |||||
const postData = { | |||||
...data, | |||||
removeUserIds: [], | |||||
removeAuthIds: [], | |||||
} | |||||
console.log(postData) | |||||
await saveGroup(postData) | |||||
router.replace("/settings/group") | |||||
} catch (e) { | |||||
console.log(e); | |||||
setServerError(t("An error has occurred. Please try again later.")); | |||||
} | |||||
}, | |||||
[router] | |||||
); | |||||
const handleCancel = () => { | |||||
router.back(); | |||||
}; | |||||
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
(_e, newValue) => { | |||||
setTabIndex(newValue); | |||||
}, | |||||
[] | |||||
); | |||||
const hasErrorsInTab = ( | |||||
tabIndex: number, | |||||
errors: FieldErrors<CreateGroupInputs>, | |||||
) => { | |||||
switch (tabIndex) { | |||||
case 0: | |||||
return Object.keys(errors).length > 0; | |||||
default: | |||||
false; | |||||
} | |||||
}; | |||||
return ( | |||||
<> | |||||
<FormProvider {...formProps}> | |||||
<Stack | |||||
spacing={2} | |||||
component="form" | |||||
onSubmit={formProps.handleSubmit(onSubmit)} | |||||
> | |||||
<Tabs | |||||
value={tabIndex} | |||||
onChange={handleTabChange} | |||||
variant="scrollable" | |||||
> | |||||
<Tab | |||||
label={t("Group Info")} | |||||
icon={ | |||||
hasErrorsInTab(0, errors) ? ( | |||||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||||
) : undefined | |||||
} | |||||
iconPosition="end" | |||||
/> | |||||
<Tab label={t("Authority Allocation")} iconPosition="end" /> | |||||
<Tab label={t("User Allocation")} iconPosition="end" /> | |||||
</Tabs> | |||||
{serverError && ( | |||||
<Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
{serverError} | |||||
</Typography> | |||||
)} | |||||
{tabIndex === 0 && <GroupInfo/>} | |||||
{tabIndex === 1 && <AuthorityAllocation auth={auth!!}/>} | |||||
{tabIndex === 2 && <UserAllocation users={users!!}/>} | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
<Button | |||||
variant="outlined" | |||||
startIcon={<Close />} | |||||
onClick={handleCancel} | |||||
> | |||||
{t("Cancel")} | |||||
</Button> | |||||
<Button | |||||
variant="contained" | |||||
startIcon={<Check />} | |||||
type="submit" | |||||
// disabled={Boolean(formProps.watch("isGridEditing"))} | |||||
> | |||||
{t("Confirm")} | |||||
</Button> | |||||
</Stack> | |||||
</Stack> | |||||
</FormProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default CreateGroup; |
@@ -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 CreateGroupLoading: React.FC = () => { | |||||
return ( | |||||
<> | |||||
<Card> | |||||
<CardContent> | |||||
<Stack spacing={2}> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton | |||||
variant="rounded" | |||||
height={50} | |||||
width={100} | |||||
sx={{ alignSelf: "flex-end" }} | |||||
/> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
<Card>Create Group | |||||
<CardContent> | |||||
<Stack spacing={2}> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
</> | |||||
); | |||||
}; | |||||
export default CreateGroupLoading; |
@@ -0,0 +1,24 @@ | |||||
import React from "react"; | |||||
import CreateGroupLoading from "./CreateGroupLoading"; | |||||
import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | |||||
import { useSearchParams } from "next/navigation"; | |||||
import CreateGroup from "./CreateGroup"; | |||||
import { auth, fetchAuth } from "@/app/api/group/actions"; | |||||
import { fetchUser } from "@/app/api/user"; | |||||
interface SubComponents { | |||||
Loading: typeof CreateGroupLoading; | |||||
} | |||||
const CreateGroupWrapper: React.FC & SubComponents = async () => { | |||||
const records = await fetchAuth() | |||||
const users = await fetchUser() | |||||
console.log(users) | |||||
const auth = records.records as auth[] | |||||
return <CreateGroup auth={auth} users={users}/>; | |||||
}; | |||||
CreateGroupWrapper.Loading = CreateGroupLoading; | |||||
export default CreateGroupWrapper; |
@@ -0,0 +1,81 @@ | |||||
"use client"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import Box from "@mui/material/Box"; | |||||
import Card from "@mui/material/Card"; | |||||
import CardContent from "@mui/material/CardContent"; | |||||
import Grid from "@mui/material/Grid"; | |||||
import TextField from "@mui/material/TextField"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import { CreateGroupInputs } from "@/app/api/group/actions"; | |||||
import { useFormContext } from "react-hook-form"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { useCallback } from "react"; | |||||
const GroupInfo: React.FC = () => { | |||||
const { t } = useTranslation(); | |||||
const { | |||||
register, | |||||
formState: { errors, defaultValues }, | |||||
control, | |||||
reset, | |||||
resetField, | |||||
setValue, | |||||
} = useFormContext<CreateGroupInputs>(); | |||||
const resetGroup = useCallback(() => { | |||||
console.log(defaultValues); | |||||
if (defaultValues !== undefined) { | |||||
resetField("description"); | |||||
} | |||||
}, [defaultValues]); | |||||
return ( | |||||
<Card sx={{ display: "block" }}> | |||||
<CardContent component={Stack} spacing={4}> | |||||
<Box> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Group Info")} | |||||
</Typography> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs={6}> | |||||
<TextField | |||||
label={t("Group Name")} | |||||
fullWidth | |||||
{...register("name", { | |||||
required: true, | |||||
})} | |||||
error={Boolean(errors.name)} | |||||
helperText={ | |||||
Boolean(errors.name) && | |||||
(errors.name?.message | |||||
? t(errors.name.message) | |||||
: t("Please input correct name")) | |||||
} | |||||
/> | |||||
</Grid> | |||||
<Grid item xs={12}> | |||||
<TextField | |||||
label={t("Group Description")} | |||||
fullWidth | |||||
multiline | |||||
rows={4} | |||||
{...register("description")} | |||||
error={Boolean(errors.description)} | |||||
helperText={ | |||||
Boolean(errors.description) && | |||||
(errors.description?.message | |||||
? t(errors.description.message) | |||||
: t("Please input correct description")) | |||||
} | |||||
/> | |||||
</Grid> | |||||
</Grid> | |||||
</Box> | |||||
</CardContent> | |||||
</Card> | |||||
); | |||||
}; | |||||
export default GroupInfo; |
@@ -0,0 +1,209 @@ | |||||
"use client"; | |||||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
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 { CreateGroupInputs, auth } from "@/app/api/group/actions"; | |||||
import SearchResults, { Column } from "../SearchResults"; | |||||
import { Add, Clear, Remove, Search } from "@mui/icons-material"; | |||||
import { UserResult } from "@/app/api/user"; | |||||
export interface Props { | |||||
users: UserResult[]; | |||||
} | |||||
const UserAllocation: React.FC<Props> = ({ users }) => { | |||||
const { t } = useTranslation(); | |||||
const { | |||||
setValue, | |||||
getValues, | |||||
formState: { defaultValues }, | |||||
reset, | |||||
resetField, | |||||
} = useFormContext<CreateGroupInputs>(); | |||||
const initialUsers = users.map((u) => ({ ...u })).sort((a, b) => a.id - b.id).filter((u) => u.groupId !== null); | |||||
const [filteredUsers, setFilteredUsers] = useState(initialUsers); | |||||
const [selectedUsers, setSelectedUsers] = useState<typeof filteredUsers>( | |||||
() => { | |||||
return filteredUsers.filter( | |||||
(s) => getValues("addUserIds")?.includes(s.id) | |||||
); | |||||
} | |||||
); | |||||
// Adding / Removing Auth | |||||
const addUser = useCallback((users: UserResult) => { | |||||
setSelectedUsers((a) => [...a, users]); | |||||
}, []); | |||||
const removeUser = useCallback((users: UserResult) => { | |||||
setSelectedUsers((a) => a.filter((a) => a.id !== users.id)); | |||||
}, []); | |||||
const clearUser = useCallback(() => { | |||||
if (defaultValues !== undefined) { | |||||
resetField("addUserIds"); | |||||
setSelectedUsers( | |||||
initialUsers.filter((s) => defaultValues.addUserIds?.includes(s.id)) | |||||
); | |||||
} | |||||
}, [defaultValues]); | |||||
// Sync with form | |||||
useEffect(() => { | |||||
setValue( | |||||
"addUserIds", | |||||
selectedUsers.map((u) => u.id) | |||||
); | |||||
}, [selectedUsers, setValue]); | |||||
const UserPoolColumns = useMemo<Column<UserResult>[]>( | |||||
() => [ | |||||
{ | |||||
label: t("Add"), | |||||
name: "id", | |||||
onClick: addUser, | |||||
buttonIcon: <Add />, | |||||
}, | |||||
{ label: t("User Name"), name: "username" }, | |||||
{ label: t("name"), name: "name" }, | |||||
], | |||||
[addUser, t] | |||||
); | |||||
const allocatedUserColumns = useMemo<Column<UserResult>[]>( | |||||
() => [ | |||||
{ | |||||
label: t("Remove"), | |||||
name: "id", | |||||
onClick: removeUser, | |||||
buttonIcon: <Remove color="warning" />, | |||||
}, | |||||
{ label: t("User Name"), name: "username" }, | |||||
{ label: t("name"), name: "name" }, | |||||
], | |||||
[removeUser, selectedUsers, t] | |||||
); | |||||
const [query, setQuery] = React.useState(""); | |||||
const onQueryInputChange = React.useCallback< | |||||
React.ChangeEventHandler<HTMLInputElement> | |||||
>((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)) | |||||
// }) | |||||
// ); | |||||
}, [users, query]); | |||||
const resetUser = React.useCallback(() => { | |||||
clearQueryInput(); | |||||
clearUser(); | |||||
}, [clearQueryInput, clearUser]); | |||||
const formProps = useForm({}); | |||||
// Tab related | |||||
const [tabIndex, setTabIndex] = React.useState(0); | |||||
const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
(_e, newValue) => { | |||||
setTabIndex(newValue); | |||||
}, | |||||
[] | |||||
); | |||||
return ( | |||||
<> | |||||
<FormProvider {...formProps}> | |||||
<Card sx={{ display: "block" }}> | |||||
<CardContent | |||||
sx={{ display: "flex", flexDirection: "column", gap: 1 }} | |||||
> | |||||
<Stack gap={2}> | |||||
<Typography variant="overline" display="block"> | |||||
{t("User")} | |||||
</Typography> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs={6} display="flex" alignItems="center"> | |||||
<Search sx={{ marginInlineEnd: 1 }} /> | |||||
<TextField | |||||
variant="standard" | |||||
fullWidth | |||||
onChange={onQueryInputChange} | |||||
value={query} | |||||
placeholder={t("Search by staff ID, name or position.")} | |||||
InputProps={{ | |||||
endAdornment: query && ( | |||||
<InputAdornment position="end"> | |||||
<IconButton onClick={clearQueryInput}> | |||||
<Clear /> | |||||
</IconButton> | |||||
</InputAdornment> | |||||
), | |||||
}} | |||||
/> | |||||
</Grid> | |||||
</Grid> | |||||
<Tabs value={tabIndex} onChange={handleTabChange}> | |||||
<Tab label={t("User Pool")} /> | |||||
<Tab | |||||
label={`${t("Allocated Users")} (${selectedUsers.length})`} | |||||
/> | |||||
</Tabs> | |||||
<Box sx={{ marginInline: -3 }}> | |||||
{tabIndex === 0 && ( | |||||
<SearchResults | |||||
noWrapper | |||||
items={differenceBy(filteredUsers, selectedUsers, "id")} | |||||
columns={UserPoolColumns} | |||||
/> | |||||
)} | |||||
{tabIndex === 1 && ( | |||||
<SearchResults | |||||
noWrapper | |||||
items={selectedUsers} | |||||
columns={allocatedUserColumns} | |||||
/> | |||||
)} | |||||
</Box> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
</FormProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default UserAllocation; |
@@ -0,0 +1 @@ | |||||
export { default } from "./CreateGroupWrapper" |
@@ -39,20 +39,20 @@ const PositionDetails: React.FC = ({ | |||||
<TextField | <TextField | ||||
label={t("Position Code")} | label={t("Position Code")} | ||||
fullWidth | fullWidth | ||||
{...register("positionCode", { | |||||
{...register("code", { | |||||
required: "Position code required!", | required: "Position code required!", | ||||
})} | })} | ||||
error={Boolean(errors.positionCode)} | |||||
error={Boolean(errors.code)} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("Position Name")} | label={t("Position Name")} | ||||
fullWidth | fullWidth | ||||
{...register("positionName", { | |||||
{...register("name", { | |||||
required: "Position name required!", | required: "Position name required!", | ||||
})} | })} | ||||
error={Boolean(errors.positionName)} | |||||
error={Boolean(errors.name)} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
@@ -1,5 +1,6 @@ | |||||
"use client"; | "use client"; | ||||
import DoneIcon from "@mui/icons-material/Done"; | |||||
import Check from "@mui/icons-material/Check"; | import Check from "@mui/icons-material/Check"; | ||||
import Close from "@mui/icons-material/Close"; | import Close from "@mui/icons-material/Close"; | ||||
import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
@@ -21,8 +22,12 @@ import { | |||||
SubmitHandler, | SubmitHandler, | ||||
useForm, | useForm, | ||||
} from "react-hook-form"; | } from "react-hook-form"; | ||||
import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions"; | |||||
import { Error } from "@mui/icons-material"; | |||||
import { | |||||
CreateProjectInputs, | |||||
deleteProject, | |||||
saveProject, | |||||
} from "@/app/api/projects/actions"; | |||||
import { Delete, Error, PlayArrow } from "@mui/icons-material"; | |||||
import { | import { | ||||
BuildingType, | BuildingType, | ||||
ContractType, | ContractType, | ||||
@@ -36,8 +41,18 @@ import { StaffResult } from "@/app/api/staff"; | |||||
import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||
import { Grade } from "@/app/api/grades"; | import { Grade } from "@/app/api/grades"; | ||||
import { Customer, Subsidiary } from "@/app/api/customer"; | import { Customer, Subsidiary } from "@/app/api/customer"; | ||||
import { isEmpty } from "lodash"; | |||||
import { | |||||
deleteDialog, | |||||
errorDialog, | |||||
submitDialog, | |||||
successDialog, | |||||
} from "../Swal/CustomAlerts"; | |||||
import dayjs from "dayjs"; | |||||
export interface Props { | export interface Props { | ||||
isEditMode: boolean; | |||||
defaultInputs?: CreateProjectInputs; | |||||
allTasks: Task[]; | allTasks: Task[]; | ||||
projectCategories: ProjectCategory[]; | projectCategories: ProjectCategory[]; | ||||
taskTemplates: TaskTemplate[]; | taskTemplates: TaskTemplate[]; | ||||
@@ -63,12 +78,22 @@ const hasErrorsInTab = ( | |||||
return ( | return ( | ||||
errors.projectName || errors.projectCode || errors.projectDescription | errors.projectName || errors.projectCode || errors.projectDescription | ||||
); | ); | ||||
case 2: | |||||
return ( | |||||
errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups | |||||
); | |||||
case 3: | |||||
return ( | |||||
errors.milestones | |||||
) | |||||
default: | default: | ||||
false; | false; | ||||
} | } | ||||
}; | }; | ||||
const CreateProject: React.FC<Props> = ({ | const CreateProject: React.FC<Props> = ({ | ||||
isEditMode, | |||||
defaultInputs, | |||||
allTasks, | allTasks, | ||||
projectCategories, | projectCategories, | ||||
taskTemplates, | taskTemplates, | ||||
@@ -90,7 +115,19 @@ const CreateProject: React.FC<Props> = ({ | |||||
const router = useRouter(); | const router = useRouter(); | ||||
const handleCancel = () => { | const handleCancel = () => { | ||||
router.back(); | |||||
router.replace("/projects"); | |||||
}; | |||||
const handleDelete = () => { | |||||
deleteDialog(async () => { | |||||
await deleteProject(formProps.getValues("projectId")!); | |||||
const clickSuccessDialog = await successDialog("Delete Success", t); | |||||
if (clickSuccessDialog) { | |||||
router.replace("/projects"); | |||||
} | |||||
}, t); | |||||
}; | }; | ||||
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | ||||
@@ -101,11 +138,102 @@ const CreateProject: React.FC<Props> = ({ | |||||
); | ); | ||||
const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>( | const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>( | ||||
async (data) => { | |||||
async (data, event) => { | |||||
try { | try { | ||||
console.log(data); | |||||
// 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) !== 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) !== 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.filter(key => Object.keys(data.taskGroups).includes(key)).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 | |||||
} | |||||
if (hasErrors) return false | |||||
// save project | |||||
setServerError(""); | setServerError(""); | ||||
await saveProject(data); | |||||
router.replace("/projects"); | |||||
let title = t("Do you want to submit?"); | |||||
let confirmButtonText = t("Submit"); | |||||
let successTitle = t("Submit Success"); | |||||
let errorTitle = t("Submit Fail"); | |||||
const buttonName = (event?.nativeEvent as any).submitter.name; | |||||
if (buttonName === "start") { | |||||
title = t("Do you want to start?"); | |||||
confirmButtonText = t("Start"); | |||||
successTitle = t("Start Success"); | |||||
errorTitle = t("Start Fail"); | |||||
} else if (buttonName === "complete") { | |||||
title = t("Do you want to complete?"); | |||||
confirmButtonText = t("Complete"); | |||||
successTitle = t("Complete Success"); | |||||
errorTitle = t("Complete Fail"); | |||||
} | |||||
submitDialog( | |||||
async () => { | |||||
if (buttonName === "start") { | |||||
data.projectActualStart = dayjs().format("YYYY-MM-DD"); | |||||
} else if (buttonName === "complete") { | |||||
data.projectActualEnd = dayjs().format("YYYY-MM-DD"); | |||||
} | |||||
const response = await saveProject(data); | |||||
if (response.id > 0) { | |||||
successDialog(successTitle, t).then(() => { | |||||
router.replace("/projects"); | |||||
}); | |||||
} else { | |||||
errorDialog(errorTitle, t).then(() => { | |||||
return false; | |||||
}); | |||||
} | |||||
}, | |||||
t, | |||||
{ title: title, confirmButtonText: confirmButtonText }, | |||||
); | |||||
} catch (e) { | } catch (e) { | ||||
setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||
} | } | ||||
@@ -115,6 +243,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | ||||
(errors) => { | (errors) => { | ||||
console.log(errors) | |||||
// Set the tab so that the focus will go there | // Set the tab so that the focus will go there | ||||
if ( | if ( | ||||
errors.projectName || | errors.projectName || | ||||
@@ -122,6 +251,10 @@ const CreateProject: React.FC<Props> = ({ | |||||
errors.projectCode | errors.projectCode | ||||
) { | ) { | ||||
setTabIndex(0); | setTabIndex(0); | ||||
} else if (errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups) { | |||||
setTabIndex(2) | |||||
} else if (errors.milestones) { | |||||
setTabIndex(3) | |||||
} | } | ||||
}, | }, | ||||
[], | [], | ||||
@@ -133,85 +266,163 @@ const CreateProject: React.FC<Props> = ({ | |||||
allocatedStaffIds: [], | allocatedStaffIds: [], | ||||
milestones: {}, | milestones: {}, | ||||
totalManhour: 0, | totalManhour: 0, | ||||
manhourPercentageByGrade: grades.reduce((acc, grade) => { | |||||
return { ...acc, [grade.id]: 1 / grades.length }; | |||||
}, {}), | |||||
...defaultInputs, | |||||
// manhourPercentageByGrade should have a sensible default | |||||
manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade) | |||||
? grades.reduce((acc, grade) => { | |||||
return { ...acc, [grade.id]: 100 / grades.length }; | |||||
}, {}) | |||||
: defaultInputs?.manhourPercentageByGrade, | |||||
}, | }, | ||||
}); | }); | ||||
const errors = formProps.formState.errors; | const errors = formProps.formState.errors; | ||||
return ( | return ( | ||||
<FormProvider {...formProps}> | |||||
<Stack | |||||
spacing={2} | |||||
component="form" | |||||
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||||
> | |||||
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||||
<Tab | |||||
label={t("Project and Client Details")} | |||||
icon={ | |||||
hasErrorsInTab(0, errors) ? ( | |||||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||||
) : undefined | |||||
} | |||||
iconPosition="end" | |||||
/> | |||||
<Tab label={t("Project Task Setup")} iconPosition="end" /> | |||||
<Tab label={t("Staff Allocation and Resource")} iconPosition="end" /> | |||||
<Tab label={t("Milestone")} iconPosition="end" /> | |||||
</Tabs> | |||||
{ | |||||
<ProjectClientDetails | |||||
buildingTypes={buildingTypes} | |||||
workNatures={workNatures} | |||||
contractTypes={contractTypes} | |||||
fundingTypes={fundingTypes} | |||||
locationTypes={locationTypes} | |||||
serviceTypes={serviceTypes} | |||||
allCustomers={allCustomers} | |||||
allSubsidiaries={allSubsidiaries} | |||||
projectCategories={projectCategories} | |||||
teamLeads={teamLeads} | |||||
isActive={tabIndex === 0} | |||||
/> | |||||
} | |||||
{ | |||||
<TaskSetup | |||||
allTasks={allTasks} | |||||
taskTemplates={taskTemplates} | |||||
isActive={tabIndex === 1} | |||||
/> | |||||
} | |||||
{ | |||||
<StaffAllocation | |||||
isActive={tabIndex === 2} | |||||
allTasks={allTasks} | |||||
grades={grades} | |||||
allStaffs={allStaffs} | |||||
/> | |||||
} | |||||
{<Milestone allTasks={allTasks} isActive={tabIndex === 3} />} | |||||
{serverError && ( | |||||
<Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
{serverError} | |||||
</Typography> | |||||
)} | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
<Button | |||||
variant="outlined" | |||||
startIcon={<Close />} | |||||
onClick={handleCancel} | |||||
<> | |||||
<FormProvider {...formProps}> | |||||
<Stack | |||||
spacing={2} | |||||
component="form" | |||||
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||||
> | |||||
{isEditMode && !(formProps.getValues("projectDeleted") === true) && ( | |||||
<Stack direction="row" gap={1}> | |||||
{!formProps.getValues("projectActualStart") && ( | |||||
<Button | |||||
name="start" | |||||
type="submit" | |||||
variant="contained" | |||||
startIcon={<PlayArrow />} | |||||
color="success" | |||||
> | |||||
{t("Start Project")} | |||||
</Button> | |||||
)} | |||||
{formProps.getValues("projectActualStart") && | |||||
!formProps.getValues("projectActualEnd") && ( | |||||
<Button | |||||
name="complete" | |||||
type="submit" | |||||
variant="contained" | |||||
startIcon={<DoneIcon />} | |||||
color="info" | |||||
> | |||||
{t("Complete Project")} | |||||
</Button> | |||||
)} | |||||
{!( | |||||
formProps.getValues("projectActualStart") && | |||||
formProps.getValues("projectActualEnd") | |||||
) && ( | |||||
<Button | |||||
variant="outlined" | |||||
startIcon={<Delete />} | |||||
color="error" | |||||
onClick={handleDelete} | |||||
> | |||||
{t("Delete Project")} | |||||
</Button> | |||||
)} | |||||
</Stack> | |||||
)} | |||||
<Tabs | |||||
value={tabIndex} | |||||
onChange={handleTabChange} | |||||
variant="scrollable" | |||||
> | > | ||||
{t("Cancel")} | |||||
</Button> | |||||
<Button variant="contained" startIcon={<Check />} type="submit"> | |||||
{t("Confirm")} | |||||
</Button> | |||||
<Tab | |||||
label={t("Project and Client Details")} | |||||
sx={{ marginInlineEnd: !hasErrorsInTab(1, errors) && (hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors)) ? 1 : undefined }} | |||||
icon={ | |||||
hasErrorsInTab(0, errors) ? ( | |||||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||||
) : undefined | |||||
} | |||||
iconPosition="end" | |||||
/> | |||||
<Tab | |||||
label={t("Project Task Setup")} | |||||
sx={{ marginInlineEnd: hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors) ? 1 : undefined }} | |||||
iconPosition="end" /> | |||||
<Tab | |||||
label={t("Staff Allocation and Resource")} | |||||
sx={{ marginInlineEnd: !hasErrorsInTab(2, errors) && hasErrorsInTab(3, errors) ? 1 : undefined }} | |||||
icon={ | |||||
hasErrorsInTab(2, errors) ? ( | |||||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||||
) : undefined | |||||
} | |||||
iconPosition="end" | |||||
/> | |||||
<Tab label={t("Milestone")} | |||||
icon={ | |||||
hasErrorsInTab(3, errors) ? ( | |||||
<Error sx={{ marginInlineEnd: 1 }} color="error" />) | |||||
: undefined} | |||||
iconPosition="end" /> | |||||
</Tabs> | |||||
{ | |||||
<ProjectClientDetails | |||||
buildingTypes={buildingTypes} | |||||
workNatures={workNatures} | |||||
contractTypes={contractTypes} | |||||
fundingTypes={fundingTypes} | |||||
locationTypes={locationTypes} | |||||
serviceTypes={serviceTypes} | |||||
allCustomers={allCustomers} | |||||
allSubsidiaries={allSubsidiaries} | |||||
projectCategories={projectCategories} | |||||
teamLeads={teamLeads} | |||||
isActive={tabIndex === 0} | |||||
/> | |||||
} | |||||
{ | |||||
<TaskSetup | |||||
allTasks={allTasks} | |||||
taskTemplates={taskTemplates} | |||||
isActive={tabIndex === 1} | |||||
/> | |||||
} | |||||
{ | |||||
<StaffAllocation | |||||
isActive={tabIndex === 2} | |||||
allTasks={allTasks} | |||||
grades={grades} | |||||
allStaffs={allStaffs} | |||||
/> | |||||
} | |||||
{<Milestone allTasks={allTasks} isActive={tabIndex === 3} />} | |||||
{serverError && ( | |||||
<Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
{serverError} | |||||
</Typography> | |||||
)} | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
<Button | |||||
variant="outlined" | |||||
startIcon={<Close />} | |||||
onClick={handleCancel} | |||||
> | |||||
{t("Cancel")} | |||||
</Button> | |||||
<Button | |||||
variant="contained" | |||||
startIcon={<Check />} | |||||
type="submit" | |||||
disabled={ | |||||
formProps.getValues("projectDeleted") === true || | |||||
(!!formProps.getValues("projectActualStart") && | |||||
!!formProps.getValues("projectActualEnd")) | |||||
} | |||||
> | |||||
{isEditMode ? t("Save") : t("Confirm")} | |||||
</Button> | |||||
</Stack> | |||||
</Stack> | </Stack> | ||||
</Stack> | |||||
</FormProvider> | |||||
</FormProvider> | |||||
</> | |||||
); | ); | ||||
}; | }; | ||||
@@ -4,6 +4,7 @@ import { | |||||
fetchProjectBuildingTypes, | fetchProjectBuildingTypes, | ||||
fetchProjectCategories, | fetchProjectCategories, | ||||
fetchProjectContractTypes, | fetchProjectContractTypes, | ||||
fetchProjectDetails, | |||||
fetchProjectFundingTypes, | fetchProjectFundingTypes, | ||||
fetchProjectLocationTypes, | fetchProjectLocationTypes, | ||||
fetchProjectServiceTypes, | fetchProjectServiceTypes, | ||||
@@ -13,7 +14,15 @@ import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | |||||
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | ||||
import { fetchGrades } from "@/app/api/grades"; | import { fetchGrades } from "@/app/api/grades"; | ||||
const CreateProjectWrapper: React.FC = async () => { | |||||
type CreateProjectProps = { isEditMode: false }; | |||||
interface EditProjectProps { | |||||
isEditMode: true; | |||||
projectId?: string; | |||||
} | |||||
type Props = CreateProjectProps | EditProjectProps; | |||||
const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
const [ | const [ | ||||
tasks, | tasks, | ||||
taskTemplates, | taskTemplates, | ||||
@@ -46,8 +55,14 @@ const CreateProjectWrapper: React.FC = async () => { | |||||
fetchGrades(), | fetchGrades(), | ||||
]); | ]); | ||||
const projectInfo = props.isEditMode | |||||
? await fetchProjectDetails(props.projectId!) | |||||
: undefined; | |||||
return ( | return ( | ||||
<CreateProject | <CreateProject | ||||
isEditMode={props.isEditMode} | |||||
defaultInputs={projectInfo} | |||||
allTasks={tasks} | allTasks={tasks} | ||||
projectCategories={projectCategories} | projectCategories={projectCategories} | ||||
taskTemplates={taskTemplates} | taskTemplates={taskTemplates} | ||||
@@ -4,7 +4,7 @@ import Card from "@mui/material/Card"; | |||||
import CardContent from "@mui/material/CardContent"; | import CardContent from "@mui/material/CardContent"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import Button from "@mui/material/Button"; | 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 CardActions from "@mui/material/CardActions"; | ||||
import RestartAlt from "@mui/icons-material/RestartAlt"; | import RestartAlt from "@mui/icons-material/RestartAlt"; | ||||
import { | import { | ||||
@@ -29,7 +29,7 @@ export interface Props { | |||||
const Milestone: React.FC<Props> = ({ allTasks, isActive }) => { | const Milestone: React.FC<Props> = ({ allTasks, isActive }) => { | ||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
const { watch } = useFormContext<CreateProjectInputs>(); | |||||
const { watch, setError, clearErrors } = useFormContext<CreateProjectInputs>(); | |||||
const currentTaskGroups = watch("taskGroups"); | const currentTaskGroups = watch("taskGroups"); | ||||
const taskGroups = useMemo( | const taskGroups = useMemo( | ||||
() => | () => | ||||
@@ -57,6 +57,35 @@ const Milestone: React.FC<Props> = ({ 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.filter(key => taskGroups.map(taskGroup => taskGroup.id).includes(parseInt(key))).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 ( | return ( | ||||
<> | <> | ||||
<Card sx={{ display: isActive ? "block" : "none" }}> | <Card sx={{ display: isActive ? "block" : "none" }}> | ||||
@@ -26,7 +26,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
import "dayjs/locale/zh-hk"; | import "dayjs/locale/zh-hk"; | ||||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | 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 { useTranslation } from "react-i18next"; | ||||
import StyledDataGrid from "../StyledDataGrid"; | import StyledDataGrid from "../StyledDataGrid"; | ||||
import { INPUT_DATE_FORMAT, moneyFormatter } from "@/app/utils/formatUtil"; | import { INPUT_DATE_FORMAT, moneyFormatter } from "@/app/utils/formatUtil"; | ||||
@@ -57,13 +57,15 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
const apiRef = useGridApiRef(); | const apiRef = useGridApiRef(); | ||||
const addRow = useCallback(() => { | 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 }]); | setPayments((p) => [...p, { id, _isNew: true }]); | ||||
setRowModesModel((model) => ({ | setRowModesModel((model) => ({ | ||||
...model, | ...model, | ||||
[id]: { mode: GridRowModes.Edit, fieldToFocus: "description" }, | [id]: { mode: GridRowModes.Edit, fieldToFocus: "description" }, | ||||
})); | })); | ||||
}, []); | |||||
}, [payments]); | |||||
const validateRow = useCallback( | const validateRow = useCallback( | ||||
(id: GridRowId) => { | (id: GridRowId) => { | ||||
@@ -239,21 +241,26 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | ||||
<Grid item xs> | <Grid item xs> | ||||
<FormControl fullWidth> | <FormControl fullWidth> | ||||
<DatePicker | |||||
label={t("Stage Start Date")} | |||||
value={startDate ? dayjs(startDate) : null} | |||||
onChange={(date) => { | |||||
if (!date) return; | |||||
const milestones = getValues("milestones"); | |||||
setValue("milestones", { | |||||
...milestones, | |||||
[taskGroupId]: { | |||||
...milestones[taskGroupId], | |||||
startDate: date.format(INPUT_DATE_FORMAT), | |||||
}, | |||||
}); | |||||
}} | |||||
/> | |||||
<DatePicker | |||||
label={t("Stage Start Date")} | |||||
value={startDate ? dayjs(startDate) : null} | |||||
onChange={(date) => { | |||||
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)), | |||||
}, | |||||
}} | |||||
/> | |||||
</FormControl> | </FormControl> | ||||
</Grid> | </Grid> | ||||
<Grid item xs> | <Grid item xs> | ||||
@@ -272,6 +279,11 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
}, | }, | ||||
}); | }); | ||||
}} | }} | ||||
slotProps={{ | |||||
textField: { | |||||
error: endDate === "Invalid Date" || new Date(startDate) > new Date(endDate) || (Boolean(formState.errors.milestones) && !Boolean(endDate)), | |||||
}, | |||||
}} | |||||
/> | /> | ||||
</FormControl> | </FormControl> | ||||
</Grid> | </Grid> | ||||
@@ -23,6 +23,7 @@ const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => { | |||||
{taskGroups.map((group, index) => { | {taskGroups.map((group, index) => { | ||||
const payments = milestones[group.id]?.payments || []; | const payments = milestones[group.id]?.payments || []; | ||||
const paymentTotal = payments.reduce((acc, p) => acc + p.amount, 0); | const paymentTotal = payments.reduce((acc, p) => acc + p.amount, 0); | ||||
projectTotal += paymentTotal; | projectTotal += paymentTotal; | ||||
return ( | return ( | ||||
@@ -41,9 +42,9 @@ const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => { | |||||
<Typography variant="h6">{t("Project Total Fee")}</Typography> | <Typography variant="h6">{t("Project Total Fee")}</Typography> | ||||
<Typography>{moneyFormatter.format(projectTotal)}</Typography> | <Typography>{moneyFormatter.format(projectTotal)}</Typography> | ||||
</Stack> | </Stack> | ||||
{projectTotal > expectedTotalFee && ( | |||||
{projectTotal !== expectedTotalFee && ( | |||||
<Typography variant="caption" color="warning.main" alignSelf="flex-end"> | <Typography variant="caption" color="warning.main" alignSelf="flex-end"> | ||||
{t("Project total fee is larger than the expected total fee!")} | |||||
{t("Project total fee should be same as the expected total fee!")} | |||||
</Typography> | </Typography> | ||||
)} | )} | ||||
</Stack> | </Stack> | ||||
@@ -45,24 +45,45 @@ const leftRightBorderCellSx: SxProps = { | |||||
borderColor: "divider", | borderColor: "divider", | ||||
}; | }; | ||||
const errorCellSx: SxProps = { | |||||
outline: "1px solid", | |||||
outlineColor: "error.main", | |||||
// borderLeft: "1px solid", | |||||
// borderRight: "1px solid", | |||||
// borderTop: "1px solid", | |||||
// borderBottom: "1px solid", | |||||
// borderColor: 'error.main' | |||||
} | |||||
const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | ||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
const { watch, register, setValue } = useFormContext<CreateProjectInputs>(); | |||||
const { watch, register, setValue, formState: { errors }, setError, clearErrors } = useFormContext<CreateProjectInputs>(); | |||||
const manhourPercentageByGrade = watch("manhourPercentageByGrade"); | const manhourPercentageByGrade = watch("manhourPercentageByGrade"); | ||||
const totalManhour = watch("totalManhour"); | const totalManhour = watch("totalManhour"); | ||||
const totalPercentage = Object.values(manhourPercentageByGrade).reduce( | |||||
const totalPercentage = Math.round(Object.values(manhourPercentageByGrade).reduce( | |||||
(acc, percent) => acc + percent, | (acc, percent) => acc + percent, | ||||
0, | 0, | ||||
); | |||||
) * 100) / 100; | |||||
const makeUpdatePercentage = useCallback( | const makeUpdatePercentage = useCallback( | ||||
(gradeId: Grade["id"]) => (percentage?: number) => { | (gradeId: Grade["id"]) => (percentage?: number) => { | ||||
if (percentage !== undefined) { | if (percentage !== undefined) { | ||||
setValue("manhourPercentageByGrade", { | |||||
const updatedManhourPercentageByGrade = { | |||||
...manhourPercentageByGrade, | ...manhourPercentageByGrade, | ||||
[gradeId]: percentage, | [gradeId]: percentage, | ||||
}); | |||||
} | |||||
setValue("manhourPercentageByGrade", updatedManhourPercentageByGrade); | |||||
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) !== 100) { | |||||
setError("manhourPercentageByGrade", {message: "manhourPercentageByGrade value is not valid", type: "invalid"}) | |||||
} else { | |||||
clearErrors("manhourPercentageByGrade") | |||||
} | |||||
} | } | ||||
}, | }, | ||||
[manhourPercentageByGrade, setValue], | [manhourPercentageByGrade, setValue], | ||||
@@ -79,7 +100,10 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | |||||
type="number" | type="number" | ||||
{...register("totalManhour", { | {...register("totalManhour", { | ||||
valueAsNumber: true, | valueAsNumber: true, | ||||
required: "totalManhour code required!", | |||||
min: 1, | |||||
})} | })} | ||||
error={Boolean(errors.totalManhour)} | |||||
/> | /> | ||||
<Box | <Box | ||||
sx={(theme) => ({ | sx={(theme) => ({ | ||||
@@ -110,15 +134,18 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | |||||
<TableCellEdit | <TableCellEdit | ||||
key={`${column.id}${idx}`} | key={`${column.id}${idx}`} | ||||
value={manhourPercentageByGrade[column.id]} | value={manhourPercentageByGrade[column.id]} | ||||
renderValue={(val) => percentFormatter.format(val)} | |||||
renderValue={(val) => val + "%"} | |||||
// renderValue={(val) => percentFormatter.format(val)} | |||||
onChange={makeUpdatePercentage(column.id)} | onChange={makeUpdatePercentage(column.id)} | ||||
convertValue={(inputValue) => Number(inputValue)} | convertValue={(inputValue) => Number(inputValue)} | ||||
cellSx={{ backgroundColor: "primary.lightest" }} | cellSx={{ backgroundColor: "primary.lightest" }} | ||||
inputSx={{ width: "3rem" }} | inputSx={{ width: "3rem" }} | ||||
error={manhourPercentageByGrade[column.id] < 0} | |||||
/> | /> | ||||
))} | ))} | ||||
<TableCell sx={leftBorderCellSx}> | |||||
{percentFormatter.format(totalPercentage)} | |||||
<TableCell sx={{ ...(totalPercentage === 100 && leftBorderCellSx), ...(totalPercentage !== 100 && {...errorCellSx, borderRight: "1px solid", borderColor: "error.main"})}}> | |||||
{totalPercentage + "%"} | |||||
{/* {percentFormatter.format(totalPercentage)} */} | |||||
</TableCell> | </TableCell> | ||||
</TableRow> | </TableRow> | ||||
<TableRow> | <TableRow> | ||||
@@ -126,7 +153,7 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | |||||
{grades.map((column, idx) => ( | {grades.map((column, idx) => ( | ||||
<TableCell key={`${column.id}${idx}`}> | <TableCell key={`${column.id}${idx}`}> | ||||
{manhourFormatter.format( | {manhourFormatter.format( | ||||
manhourPercentageByGrade[column.id] * totalManhour, | |||||
manhourPercentageByGrade[column.id] / 100 * totalManhour, | |||||
)} | )} | ||||
</TableCell> | </TableCell> | ||||
))} | ))} | ||||
@@ -144,7 +171,7 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | |||||
const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | ||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
const { watch, setValue } = useFormContext<CreateProjectInputs>(); | |||||
const { watch, setValue, clearErrors, setError } = useFormContext<CreateProjectInputs>(); | |||||
const currentTaskGroups = watch("taskGroups"); | const currentTaskGroups = watch("taskGroups"); | ||||
const taskGroups = useMemo( | const taskGroups = useMemo( | ||||
@@ -167,13 +194,22 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||||
const makeUpdatePercentage = useCallback( | const makeUpdatePercentage = useCallback( | ||||
(taskGroupId: TaskGroup["id"]) => (percentage?: number) => { | (taskGroupId: TaskGroup["id"]) => (percentage?: number) => { | ||||
if (percentage !== undefined) { | if (percentage !== undefined) { | ||||
setValue("taskGroups", { | |||||
const updatedTaskGroups = { | |||||
...currentTaskGroups, | ...currentTaskGroups, | ||||
[taskGroupId]: { | [taskGroupId]: { | ||||
...currentTaskGroups[taskGroupId], | ...currentTaskGroups[taskGroupId], | ||||
percentAllocation: percentage, | percentAllocation: percentage, | ||||
}, | }, | ||||
}); | |||||
} | |||||
setValue("taskGroups", updatedTaskGroups); | |||||
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) !== 100) { | |||||
setError("taskGroups", {message: "Task Groups value is not invalid", type: "invalid"}) | |||||
} else { | |||||
clearErrors("taskGroups") | |||||
} | |||||
} | } | ||||
}, | }, | ||||
[currentTaskGroups, setValue], | [currentTaskGroups, setValue], | ||||
@@ -216,24 +252,28 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||||
</TableCell> | </TableCell> | ||||
<TableCellEdit | <TableCellEdit | ||||
value={currentTaskGroups[tg.id].percentAllocation} | value={currentTaskGroups[tg.id].percentAllocation} | ||||
renderValue={(val) => percentFormatter.format(val)} | |||||
// renderValue={(val) => percentFormatter.format(val)} | |||||
renderValue={(val) => val + "%"} | |||||
onChange={makeUpdatePercentage(tg.id)} | onChange={makeUpdatePercentage(tg.id)} | ||||
convertValue={(inputValue) => Number(inputValue)} | convertValue={(inputValue) => Number(inputValue)} | ||||
cellSx={{ backgroundColor: "primary.lightest" }} | |||||
cellSx={{ | |||||
backgroundColor: "primary.lightest", | |||||
}} | |||||
inputSx={{ width: "3rem" }} | inputSx={{ width: "3rem" }} | ||||
error={currentTaskGroups[tg.id].percentAllocation < 0} | |||||
/> | /> | ||||
<TableCell sx={rightBorderCellSx}> | <TableCell sx={rightBorderCellSx}> | ||||
{manhourFormatter.format( | {manhourFormatter.format( | ||||
currentTaskGroups[tg.id].percentAllocation * totalManhour, | |||||
currentTaskGroups[tg.id].percentAllocation / 100 * totalManhour, | |||||
)} | )} | ||||
</TableCell> | </TableCell> | ||||
{grades.map((column, idx) => { | {grades.map((column, idx) => { | ||||
const stageHours = | const stageHours = | ||||
currentTaskGroups[tg.id].percentAllocation * totalManhour; | |||||
currentTaskGroups[tg.id].percentAllocation / 100 * totalManhour; | |||||
return ( | return ( | ||||
<TableCell key={`${column.id}${idx}`}> | <TableCell key={`${column.id}${idx}`}> | ||||
{manhourFormatter.format( | {manhourFormatter.format( | ||||
manhourPercentageByGrade[column.id] * stageHours, | |||||
manhourPercentageByGrade[column.id] / 100 * stageHours, | |||||
)} | )} | ||||
</TableCell> | </TableCell> | ||||
); | ); | ||||
@@ -248,10 +288,14 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||||
0, | 0, | ||||
)} | )} | ||||
</TableCell> | </TableCell> | ||||
<TableCell sx={leftBorderCellSx}> | |||||
<TableCell sx={{ | |||||
...(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( | {percentFormatter.format( | ||||
Object.values(currentTaskGroups).reduce( | Object.values(currentTaskGroups).reduce( | ||||
(acc, tg) => acc + tg.percentAllocation, | |||||
(acc, tg) => acc + tg.percentAllocation / 100, | |||||
0, | 0, | ||||
), | ), | ||||
)} | )} | ||||
@@ -259,7 +303,7 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||||
<TableCell sx={rightBorderCellSx}> | <TableCell sx={rightBorderCellSx}> | ||||
{manhourFormatter.format( | {manhourFormatter.format( | ||||
Object.values(currentTaskGroups).reduce( | Object.values(currentTaskGroups).reduce( | ||||
(acc, tg) => acc + tg.percentAllocation * totalManhour, | |||||
(acc, tg) => acc + tg.percentAllocation / 100 * totalManhour, | |||||
0, | 0, | ||||
), | ), | ||||
)} | )} | ||||
@@ -268,9 +312,9 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||||
const hours = Object.values(currentTaskGroups).reduce( | const hours = Object.values(currentTaskGroups).reduce( | ||||
(acc, tg) => | (acc, tg) => | ||||
acc + | acc + | ||||
tg.percentAllocation * | |||||
totalManhour * | |||||
manhourPercentageByGrade[column.id], | |||||
tg.percentAllocation / 100 * | |||||
totalManhour * | |||||
manhourPercentageByGrade[column.id] / 100 , | |||||
0, | 0, | ||||
); | ); | ||||
return ( | return ( | ||||
@@ -52,7 +52,7 @@ const TaskSetup: React.FC<Props> = ({ | |||||
(e: SelectChangeEvent<number | "All">) => { | (e: SelectChangeEvent<number | "All">) => { | ||||
if (e.target.value === "All" || isNumber(e.target.value)) { | if (e.target.value === "All" || isNumber(e.target.value)) { | ||||
setSelectedTaskTemplateId(e.target.value); | setSelectedTaskTemplateId(e.target.value); | ||||
onReset(); | |||||
// onReset(); | |||||
} | } | ||||
}, | }, | ||||
[onReset], | [onReset], | ||||
@@ -22,7 +22,6 @@ import { fetchSkillCombo } from "@/app/api/skill/actions"; | |||||
import { fetchSalaryCombo } from "@/app/api/salarys/actions"; | import { fetchSalaryCombo } from "@/app/api/salarys/actions"; | ||||
interface Field { | interface Field { | ||||
// subtitle: string; | |||||
id: string; | id: string; | ||||
label: string; | label: string; | ||||
type: string; | type: string; | ||||
@@ -33,12 +32,6 @@ interface Field { | |||||
options?: any[]; | options?: any[]; | ||||
readOnly?: boolean; | readOnly?: boolean; | ||||
} | } | ||||
interface formProps { | |||||
Title?: string[]; | |||||
// fieldLists: Field[][]; | |||||
} | |||||
export interface comboItem { | export interface comboItem { | ||||
company: comboProp[]; | company: comboProp[]; | ||||
team: comboProp[]; | team: comboProp[]; | ||||
@@ -49,101 +42,14 @@ export interface comboItem { | |||||
salary: comboProp[]; | salary: comboProp[]; | ||||
} | } | ||||
const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
// const router = useRouter(); | |||||
const { t } = useTranslation(); | |||||
const [companyCombo, setCompanyCombo] = useState<comboProp[]>(); | |||||
const [teamCombo, setTeamCombo] = useState<comboProp[]>(); | |||||
const [departmentCombo, setDepartmentCombo] = useState<comboProp[]>(); | |||||
const [positionCombo, setPositionCombo] = useState<comboProp[]>(); | |||||
const [gradeCombo, setGradeCombo] = useState<comboProp[]>(); | |||||
const [skillCombo, setSkillCombo] = useState<comboProp[]>(); | |||||
const [salaryCombo, setSalaryCombo] = useState<comboProp[]>(); | |||||
// const [serverError, setServerError] = useState(""); | |||||
let comboItem: comboItem = { | |||||
company: [], | |||||
team: [], | |||||
department: [], | |||||
position: [], | |||||
grade: [], | |||||
skill: [], | |||||
salary: [], | |||||
}; | |||||
const fetchCompany = async () => { | |||||
await fetchCompanyCombo().then((data) => { | |||||
if (data) setCompanyCombo(data.records); | |||||
}); | |||||
} | |||||
const fetchTeam = async () => { | |||||
await fetchTeamCombo().then((data) => { | |||||
if (data) setTeamCombo(data.records); | |||||
}); | |||||
} | |||||
const fetchDepartment = async () => { | |||||
await fetchDepartmentCombo().then((data) => { | |||||
if (data) setDepartmentCombo(data.records); | |||||
}); | |||||
} | |||||
const fetchPosition = async () => { | |||||
await fetchPositionCombo().then((data) => { | |||||
if (data) setPositionCombo(data.records); | |||||
}); | |||||
} | |||||
const fetchGrade = async () => { | |||||
await fetchGradeCombo().then((data) => { | |||||
if (data) setGradeCombo(data.records); | |||||
}); | |||||
} | |||||
const fetchSkill = async () => { | |||||
await fetchSkillCombo().then((data) => { | |||||
if (data) setSkillCombo(data.records); | |||||
}); | |||||
} | |||||
const fetchSalary = async () => { | |||||
await fetchSalaryCombo().then((data) => { | |||||
if (data) setSalaryCombo(data.records); | |||||
}); | |||||
} | |||||
useEffect(() => { | |||||
fetchCompany() | |||||
fetchTeam() | |||||
fetchDepartment() | |||||
fetchPosition() | |||||
fetchGrade() | |||||
fetchSkill() | |||||
fetchSalary() | |||||
}, []); | |||||
useEffect(() => { | |||||
if(!companyCombo) | |||||
fetchCompany() | |||||
if(!teamCombo) | |||||
fetchTeam() | |||||
if(!departmentCombo) | |||||
fetchDepartment() | |||||
if(!positionCombo) | |||||
fetchPosition() | |||||
if(!gradeCombo) | |||||
fetchGrade() | |||||
if(!skillCombo) | |||||
fetchSkill() | |||||
if(!salaryCombo) | |||||
fetchSalary() | |||||
interface formProps { | |||||
Title?: string[]; | |||||
combos: comboItem; | |||||
} | |||||
}, [companyCombo, teamCombo, departmentCombo, positionCombo, gradeCombo, skillCombo, salaryCombo]); | |||||
// useEffect(() => { | |||||
// console.log(companyCombo) | |||||
// }, [companyCombo]); | |||||
const CreateStaff: React.FC<formProps> = ({ Title, combos }) => { | |||||
const { t } = useTranslation(); | |||||
const fieldLists: Field[][] = [ | const fieldLists: Field[][] = [ | ||||
[ | [ | ||||
@@ -163,49 +69,49 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
id: "companyId", | id: "companyId", | ||||
label: t("Company"), | label: t("Company"), | ||||
type: "combo-Obj", | type: "combo-Obj", | ||||
options: companyCombo || [], | |||||
options: combos.company || [], | |||||
required: true, | required: true, | ||||
}, | }, | ||||
{ | { | ||||
id: "teamId", | id: "teamId", | ||||
label: t("Team"), | label: t("Team"), | ||||
type: "combo-Obj", | type: "combo-Obj", | ||||
options: teamCombo || [], | |||||
options: combos.team || [], | |||||
required: false, | required: false, | ||||
}, | }, | ||||
{ | { | ||||
id: "departmentId", | id: "departmentId", | ||||
label: t("Department"), | label: t("Department"), | ||||
type: "combo-Obj", | type: "combo-Obj", | ||||
options: departmentCombo || [], | |||||
options: combos.department || [], | |||||
required: true, | required: true, | ||||
}, | }, | ||||
{ | { | ||||
id: "gradeId", | id: "gradeId", | ||||
label: t("Grade"), | label: t("Grade"), | ||||
type: "combo-Obj", | type: "combo-Obj", | ||||
options: gradeCombo || [], | |||||
options: combos.grade || [], | |||||
required: false, | required: false, | ||||
}, | }, | ||||
{ | { | ||||
id: "skillSetId", | id: "skillSetId", | ||||
label: t("Skillset"), | label: t("Skillset"), | ||||
type: "multiSelect-Obj", | type: "multiSelect-Obj", | ||||
options: skillCombo || [], | |||||
options: combos.skill || [], | |||||
required: false, | required: false, | ||||
}, | }, | ||||
{ | { | ||||
id: "currentPositionId", | id: "currentPositionId", | ||||
label: t("Current Position"), | label: t("Current Position"), | ||||
type: "combo-Obj", | type: "combo-Obj", | ||||
options: positionCombo || [], | |||||
options: combos.position || [], | |||||
required: true, | required: true, | ||||
}, | }, | ||||
{ | { | ||||
id: "salaryId", | id: "salaryId", | ||||
label: t("Salary Point"), | label: t("Salary Point"), | ||||
type: "combo-Obj", | type: "combo-Obj", | ||||
options: salaryCombo || [], | |||||
options: combos.salary || [], | |||||
required: true, | required: true, | ||||
}, | }, | ||||
// { | // { | ||||
@@ -279,7 +185,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
id: "joinPositionId", | id: "joinPositionId", | ||||
label: t("Join Position"), | label: t("Join Position"), | ||||
type: "combo-Obj", | type: "combo-Obj", | ||||
options: positionCombo || [], | |||||
options: combos.position || [], | |||||
required: true, | required: true, | ||||
}, | }, | ||||
{ | { | ||||
@@ -1,17 +1,48 @@ | |||||
import React from "react"; | import React from "react"; | ||||
import CreateStaff from "./CreateStaff"; | |||||
import CreateStaff, { comboItem } from "./CreateStaff"; | |||||
import CreateStaffLoading from "./CreateStaffLoading"; | import CreateStaffLoading from "./CreateStaffLoading"; | ||||
import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | ||||
import { useSearchParams } from "next/navigation"; | import { useSearchParams } from "next/navigation"; | ||||
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 { fetchCompanyCombo } from "@/app/api/companys/actions"; | |||||
interface SubComponents { | interface SubComponents { | ||||
Loading: typeof CreateStaffLoading; | Loading: typeof CreateStaffLoading; | ||||
} | } | ||||
const CreateStaffWrapper: React.FC & SubComponents = async () => { | const CreateStaffWrapper: React.FC & SubComponents = async () => { | ||||
const [ | |||||
CompanyCombo, | |||||
TeamCombo, | |||||
DepartmentCombo, | |||||
PositionCombo, | |||||
GradeCombo, | |||||
SkillCombo, | |||||
SalaryCombo, | |||||
] = await Promise.all([ | |||||
fetchCompanyCombo(), | |||||
fetchTeamCombo(), | |||||
fetchDepartmentCombo(), | |||||
fetchPositionCombo(), | |||||
fetchGradeCombo(), | |||||
fetchSkillCombo(), | |||||
fetchSalaryCombo(), | |||||
]); | |||||
const combos: comboItem = { | |||||
company: CompanyCombo.records, | |||||
team: TeamCombo.records, | |||||
department: DepartmentCombo.records, | |||||
position: PositionCombo.records, | |||||
grade: GradeCombo.records, | |||||
skill: SkillCombo.records, | |||||
salary: SalaryCombo.records, | |||||
} | |||||
return <CreateStaff/>; | |||||
return <CreateStaff combos={combos}/>; | |||||
}; | }; | ||||
CreateStaffWrapper.Loading = CreateStaffLoading; | CreateStaffWrapper.Loading = CreateStaffLoading; | ||||
@@ -10,26 +10,31 @@ import TransferList from "../TransferList"; | |||||
import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
import Check from "@mui/icons-material/Check"; | import Check from "@mui/icons-material/Check"; | ||||
import Close from "@mui/icons-material/Close"; | import Close from "@mui/icons-material/Close"; | ||||
import { useRouter, useSearchParams } from "next/navigation"; | |||||
import { useRouter } from "next/navigation"; | |||||
import React from "react"; | import React from "react"; | ||||
import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
import { Task } from "@/app/api/tasks"; | |||||
import { Task, TaskTemplate } from "@/app/api/tasks"; | |||||
import { | import { | ||||
NewTaskTemplateFormInputs, | NewTaskTemplateFormInputs, | ||||
fetchTaskTemplate, | |||||
saveTaskTemplate, | saveTaskTemplate, | ||||
} from "@/app/api/tasks/actions"; | } from "@/app/api/tasks/actions"; | ||||
import { SubmitHandler, useFieldArray, useForm } from "react-hook-form"; | |||||
import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||||
import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts"; | import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts"; | ||||
import { Grade } from "@/app/api/grades"; | |||||
import { intersectionWith, isEmpty } from "lodash"; | |||||
import ResourceAllocationWrapper from "./ResourceAllocation"; | |||||
interface Props { | interface Props { | ||||
tasks: Task[]; | tasks: Task[]; | ||||
defaultInputs?: NewTaskTemplateFormInputs; | |||||
grades: Grade[] | |||||
} | } | ||||
const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||||
const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs, grades }) => { | |||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
const searchParams = useSearchParams() | |||||
const router = useRouter(); | const router = useRouter(); | ||||
const handleCancel = () => { | const handleCancel = () => { | ||||
router.back(); | router.back(); | ||||
@@ -47,57 +52,53 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||||
const [serverError, setServerError] = React.useState(""); | const [serverError, setServerError] = React.useState(""); | ||||
const { | |||||
register, | |||||
handleSubmit, | |||||
setValue, | |||||
watch, | |||||
resetField, | |||||
formState: { errors, isSubmitting }, | |||||
} = useForm<NewTaskTemplateFormInputs>({ defaultValues: { taskIds: [] } }); | |||||
const currentTaskIds = watch("taskIds"); | |||||
const selectedItems = React.useMemo(() => { | |||||
return items.filter((item) => currentTaskIds.includes(item.id)); | |||||
}, [currentTaskIds, items]); | |||||
const [refTaskTemplate, setRefTaskTemplate] = React.useState<NewTaskTemplateFormInputs>() | |||||
const id = searchParams.get('id') | |||||
const fetchCurrentTaskTemplate = async () => { | |||||
try { | |||||
const taskTemplate = await fetchTaskTemplate(parseInt(id!!)) | |||||
const formProps = useForm<NewTaskTemplateFormInputs>({ | |||||
defaultValues: { | |||||
taskGroups: {}, | |||||
...defaultInputs, | |||||
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) | |||||
manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade) | |||||
? grades.reduce((acc, grade) => { | |||||
return { ...acc, [grade.id]: 100 / grades.length }; | |||||
}, {}) | |||||
: defaultInputs?.manhourPercentageByGrade, | |||||
} | } | ||||
}, [refTaskTemplate]) | |||||
}); | |||||
const currentTaskGroups = formProps.watch("taskGroups"); | |||||
const currentTaskIds = Object.values(currentTaskGroups).reduce<Task["id"][]>( | |||||
(acc, group) => { | |||||
return [...acc, ...group.taskIds]; | |||||
}, | |||||
[], | |||||
); | |||||
const selectedItems = React.useMemo(() => { | |||||
return intersectionWith( | |||||
tasks, | |||||
currentTaskIds, | |||||
(task, taskId) => task.id === taskId, | |||||
).map((t) => ({ id: t.id, label: t.name, group: t.taskGroup })); | |||||
}, [currentTaskIds, tasks]); | |||||
const onSubmit: SubmitHandler<NewTaskTemplateFormInputs> = React.useCallback( | const onSubmit: SubmitHandler<NewTaskTemplateFormInputs> = React.useCallback( | ||||
async (data) => { | async (data) => { | ||||
try { | try { | ||||
console.log(data) | |||||
setServerError(""); | setServerError(""); | ||||
let hasErrors = false | |||||
// check the manhour allocation by stage by grade -> total = 100? | |||||
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) !== 100) { | |||||
formProps.setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" }) | |||||
hasErrors = true | |||||
} | |||||
if (hasErrors) return false | |||||
submitDialog(async () => { | submitDialog(async () => { | ||||
const response = await saveTaskTemplate(data); | const response = await saveTaskTemplate(data); | ||||
@@ -120,8 +121,9 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||||
return ( | return ( | ||||
<> | <> | ||||
{ | |||||
(id === null || refTaskTemplate !== undefined) && <Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}> | |||||
<FormProvider {...formProps}> | |||||
<Stack component="form" onSubmit={formProps.handleSubmit(onSubmit)} gap={2}> | |||||
{/* Task List Setup */} | |||||
<Card> | <Card> | ||||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | ||||
<Typography variant="overline">{t("Task List Setup")}</Typography> | <Typography variant="overline">{t("Task List Setup")}</Typography> | ||||
@@ -135,22 +137,22 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||||
<TextField | <TextField | ||||
label={t("Task Template Code")} | label={t("Task Template Code")} | ||||
fullWidth | fullWidth | ||||
{...register("code", { | |||||
{...formProps.register("code", { | |||||
required: t("Task template code is required"), | required: t("Task template code is required"), | ||||
})} | })} | ||||
error={Boolean(errors.code?.message)} | |||||
helperText={errors.code?.message} | |||||
error={Boolean(formProps.formState.errors.code?.message)} | |||||
helperText={formProps.formState.errors.code?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("Task Template Name")} | label={t("Task Template Name")} | ||||
fullWidth | fullWidth | ||||
{...register("name", { | |||||
{...formProps.register("name", { | |||||
required: t("Task template name is required"), | required: t("Task template name is required"), | ||||
})} | })} | ||||
error={Boolean(errors.name?.message)} | |||||
helperText={errors.name?.message} | |||||
error={Boolean(formProps.formState.errors.name?.message)} | |||||
helperText={formProps.formState.errors.name?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
</Grid> | </Grid> | ||||
@@ -158,16 +160,54 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||||
allItems={items} | allItems={items} | ||||
selectedItems={selectedItems} | selectedItems={selectedItems} | ||||
onChange={(selectedTasks) => { | onChange={(selectedTasks) => { | ||||
setValue( | |||||
"taskIds", | |||||
selectedTasks.map((item) => item.id), | |||||
); | |||||
// formProps.setValue( | |||||
// "taskIds", | |||||
// selectedTasks.map((item) => item.id), | |||||
// ); | |||||
const newTaskGroups = selectedTasks.reduce< | |||||
NewTaskTemplateFormInputs["taskGroups"] | |||||
>((acc, item) => { | |||||
if (!item.group) { | |||||
// TODO: this should not happen (all tasks are part of a group) | |||||
return acc; | |||||
} | |||||
if (!acc[item.group.id]) { | |||||
return { | |||||
...acc, | |||||
[item.group.id]: { | |||||
taskIds: [item.id], | |||||
percentAllocation: | |||||
currentTaskGroups[item.group.id]?.percentAllocation || 0, | |||||
}, | |||||
}; | |||||
} | |||||
return { | |||||
...acc, | |||||
[item.group.id]: { | |||||
...acc[item.group.id], | |||||
taskIds: [...acc[item.group.id].taskIds, item.id], | |||||
}, | |||||
}; | |||||
}, {}); | |||||
formProps.setValue("taskGroups", newTaskGroups); | |||||
}} | }} | ||||
allItemsLabel={t("Task Pool")} | allItemsLabel={t("Task Pool")} | ||||
selectedItemsLabel={t("Task List Template")} | selectedItemsLabel={t("Task List Template")} | ||||
/> | /> | ||||
</CardContent> | </CardContent> | ||||
</Card> | </Card> | ||||
{/* Resource Allocation */} | |||||
<Card> | |||||
<CardContent> | |||||
<ResourceAllocationWrapper | |||||
allTasks={tasks} | |||||
grades={grades} | |||||
/> | |||||
</CardContent> | |||||
</Card> | |||||
{ | { | ||||
serverError && ( | serverError && ( | ||||
<Typography variant="body2" color="error" alignSelf="flex-end"> | <Typography variant="body2" color="error" alignSelf="flex-end"> | ||||
@@ -183,12 +223,13 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||||
variant="contained" | variant="contained" | ||||
startIcon={<Check />} | startIcon={<Check />} | ||||
type="submit" | type="submit" | ||||
disabled={isSubmitting} | |||||
disabled={formProps.formState.isSubmitting} | |||||
> | > | ||||
{t("Confirm")} | {t("Confirm")} | ||||
</Button> | </Button> | ||||
</Stack> | </Stack> | ||||
</Stack >} | |||||
</Stack > | |||||
</FormProvider> | |||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
@@ -1,11 +1,20 @@ | |||||
import React from "react"; | import React from "react"; | ||||
import CreateTaskTemplate from "./CreateTaskTemplate"; | import CreateTaskTemplate from "./CreateTaskTemplate"; | ||||
import { fetchAllTasks } from "@/app/api/tasks"; | |||||
import { fetchAllTasks, fetchTaskTemplateDetail } from "@/app/api/tasks"; | |||||
import { fetchGrades } from "@/app/api/grades"; | |||||
const CreateTaskTemplateWrapper: React.FC = async () => { | |||||
const tasks = await fetchAllTasks(); | |||||
interface Props { | |||||
taskTemplateId?: string; | |||||
} | |||||
return <CreateTaskTemplate tasks={tasks} />; | |||||
const CreateTaskTemplateWrapper: React.FC<Props> = async (props) => { | |||||
const [tasks, grades] = await Promise.all([ | |||||
fetchAllTasks(), | |||||
fetchGrades(), | |||||
]); | |||||
const taskTemplateInfo = props.taskTemplateId ? await fetchTaskTemplateDetail(props.taskTemplateId) : undefined | |||||
return <CreateTaskTemplate tasks={tasks} grades={grades} defaultInputs={taskTemplateInfo}/>; | |||||
}; | }; | ||||
export default CreateTaskTemplateWrapper; | export default CreateTaskTemplateWrapper; |
@@ -0,0 +1,287 @@ | |||||
import { Task, TaskGroup } from "@/app/api/tasks"; | |||||
import { | |||||
Box, | |||||
Typography, | |||||
TextField, | |||||
Alert, | |||||
TableContainer, | |||||
Table, | |||||
TableBody, | |||||
TableCell, | |||||
TableHead, | |||||
TableRow, | |||||
Stack, | |||||
SxProps, | |||||
} from "@mui/material"; | |||||
import React, { useCallback, useMemo } from "react"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import uniqBy from "lodash/uniqBy"; | |||||
import { Grade } from "@/app/api/grades"; | |||||
import { percentFormatter } from "@/app/utils/formatUtil"; | |||||
import TableCellEdit from "../TableCellEdit"; | |||||
import { useFormContext } from "react-hook-form"; | |||||
import { NewTaskTemplateFormInputs } from "@/app/api/tasks/actions"; | |||||
interface Props { | |||||
allTasks: Task[]; | |||||
grades: Grade[]; | |||||
} | |||||
const leftBorderCellSx: SxProps = { | |||||
borderLeft: "1px solid", | |||||
borderColor: "divider", | |||||
}; | |||||
const rightBorderCellSx: SxProps = { | |||||
borderRight: "1px solid", | |||||
borderColor: "divider", | |||||
}; | |||||
const leftRightBorderCellSx: SxProps = { | |||||
borderLeft: "1px solid", | |||||
borderRight: "1px solid", | |||||
borderColor: "divider", | |||||
}; | |||||
const errorCellSx: SxProps = { | |||||
outline: "1px solid", | |||||
outlineColor: "error.main", | |||||
} | |||||
const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | |||||
const { t } = useTranslation(); | |||||
const { watch, register, setValue, formState: { errors }, setError, clearErrors } = useFormContext<NewTaskTemplateFormInputs>(); | |||||
const manhourPercentageByGrade = watch("manhourPercentageByGrade"); | |||||
const totalPercentage = Math.round(Object.values(manhourPercentageByGrade).reduce( | |||||
(acc, percent) => acc + percent, | |||||
0, | |||||
) * 100) / 100; | |||||
const makeUpdatePercentage = useCallback( | |||||
(gradeId: Grade["id"]) => (percentage?: number) => { | |||||
if (percentage !== undefined) { | |||||
const updatedManhourPercentageByGrade = { | |||||
...manhourPercentageByGrade, | |||||
[gradeId]: percentage, | |||||
} | |||||
setValue("manhourPercentageByGrade", updatedManhourPercentageByGrade); | |||||
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) !== 100) { | |||||
setError("manhourPercentageByGrade", { message: "manhourPercentageByGrade value is not valid", type: "invalid" }) | |||||
} else { | |||||
clearErrors("manhourPercentageByGrade") | |||||
} | |||||
} | |||||
}, | |||||
[manhourPercentageByGrade, setValue], | |||||
); | |||||
return ( | |||||
<Box> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Manhour Allocation By Grade")} | |||||
</Typography> | |||||
<Box | |||||
sx={(theme) => ({ | |||||
marginBlockStart: 2, | |||||
marginInline: -3, | |||||
borderBottom: `1px solid ${theme.palette.divider}`, | |||||
})} | |||||
> | |||||
<TableContainer sx={{ maxHeight: 440 }}> | |||||
<Table> | |||||
<TableHead> | |||||
<TableRow> | |||||
<TableCell sx={rightBorderCellSx}> | |||||
{t("Allocation Type")} | |||||
</TableCell> | |||||
{grades.map((column, idx) => ( | |||||
<TableCell key={`${column.id}${idx}`}> | |||||
{column.name} | |||||
</TableCell> | |||||
))} | |||||
<TableCell sx={leftBorderCellSx}>{t("Total")}</TableCell> | |||||
</TableRow> | |||||
</TableHead> | |||||
<TableBody> | |||||
<TableRow> | |||||
<TableCell sx={rightBorderCellSx}>{t("Percentage")}</TableCell> | |||||
{grades.map((column, idx) => ( | |||||
<TableCellEdit | |||||
key={`${column.id}${idx}`} | |||||
value={manhourPercentageByGrade[column.id]} | |||||
renderValue={(val) => val + "%"} | |||||
onChange={makeUpdatePercentage(column.id)} | |||||
convertValue={(inputValue) => Number(inputValue)} | |||||
cellSx={{ backgroundColor: "primary.lightest" }} | |||||
inputSx={{ width: "3rem" }} | |||||
error={manhourPercentageByGrade[column.id] < 0} | |||||
/> | |||||
))} | |||||
<TableCell sx={{ ...(totalPercentage === 100 && leftBorderCellSx), ...(totalPercentage !== 100 && { ...errorCellSx, borderRight: "1px solid", borderColor: "error.main" }) }}> | |||||
{totalPercentage + "%"} | |||||
</TableCell> | |||||
</TableRow> | |||||
</TableBody> | |||||
</Table> | |||||
</TableContainer> | |||||
</Box> | |||||
</Box> | |||||
); | |||||
}; | |||||
const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||||
const { t } = useTranslation(); | |||||
const { watch, setValue, clearErrors, setError } = useFormContext<NewTaskTemplateFormInputs>(); | |||||
const currentTaskGroups = watch("taskGroups"); | |||||
const taskGroups = useMemo( | |||||
() => | |||||
uniqBy( | |||||
allTasks.reduce<TaskGroup[]>((acc, task) => { | |||||
if (currentTaskGroups[task.taskGroup.id]) { | |||||
return [...acc, task.taskGroup]; | |||||
} | |||||
return acc; | |||||
}, []), | |||||
"id", | |||||
), | |||||
[allTasks, currentTaskGroups], | |||||
); | |||||
const manhourPercentageByGrade = watch("manhourPercentageByGrade"); | |||||
const makeUpdatePercentage = useCallback( | |||||
(taskGroupId: TaskGroup["id"]) => (percentage?: number) => { | |||||
console.log(percentage) | |||||
if (percentage !== undefined) { | |||||
const updatedTaskGroups = { | |||||
...currentTaskGroups, | |||||
[taskGroupId]: { | |||||
...currentTaskGroups[taskGroupId], | |||||
percentAllocation: percentage, | |||||
}, | |||||
} | |||||
console.log(updatedTaskGroups) | |||||
setValue("taskGroups", updatedTaskGroups); | |||||
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) !== 100) { | |||||
setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" }) | |||||
} else { | |||||
clearErrors("taskGroups") | |||||
} | |||||
} | |||||
}, | |||||
[currentTaskGroups, setValue], | |||||
); | |||||
return ( | |||||
<Box> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Manhour Allocation By Stage By Grade")} | |||||
</Typography> | |||||
<Box | |||||
sx={(theme) => ({ | |||||
marginBlockStart: 2, | |||||
marginInline: -3, | |||||
borderBottom: `1px solid ${theme.palette.divider}`, | |||||
})} | |||||
> | |||||
<TableContainer sx={{ maxHeight: 440 }}> | |||||
<Table> | |||||
<TableHead> | |||||
<TableRow> | |||||
<TableCell>{t("Stage")}</TableCell> | |||||
<TableCell sx={leftBorderCellSx}>{t("Task Count")}</TableCell> | |||||
<TableCell colSpan={2} sx={leftRightBorderCellSx}> | |||||
{t("Total Manhour")} | |||||
</TableCell> | |||||
</TableRow> | |||||
</TableHead> | |||||
<TableBody> | |||||
{taskGroups.map((tg, idx) => ( | |||||
<TableRow key={`${tg.id}${idx}`}> | |||||
<TableCell>{tg.name}</TableCell> | |||||
<TableCell sx={leftBorderCellSx}> | |||||
{currentTaskGroups[tg.id].taskIds.length} | |||||
</TableCell> | |||||
<TableCellEdit | |||||
value={currentTaskGroups[tg.id].percentAllocation} | |||||
// renderValue={(val) => percentFormatter.format(val)} | |||||
renderValue={(val) => val + "%"} | |||||
onChange={makeUpdatePercentage(tg.id)} | |||||
convertValue={(inputValue) => Number(inputValue)} | |||||
cellSx={{ | |||||
backgroundColor: "primary.lightest", | |||||
...(currentTaskGroups[tg.id].percentAllocation < 0 && { ...errorCellSx, borderBottom: "0px", borderRight: "1px solid", borderColor: "error.main"}) | |||||
}} | |||||
inputSx={{ width: "3rem" }} | |||||
error={currentTaskGroups[tg.id].percentAllocation < 0} | |||||
/> | |||||
</TableRow> | |||||
))} | |||||
<TableRow> | |||||
<TableCell>{t("Total")}</TableCell> | |||||
<TableCell sx={leftBorderCellSx}> | |||||
{Object.values(currentTaskGroups).reduce( | |||||
(acc, tg) => acc + tg.taskIds.length, | |||||
0, | |||||
)} | |||||
</TableCell> | |||||
<TableCell sx={{ | |||||
...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) === 100 && leftBorderCellSx), | |||||
...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) !== 100 && { ...errorCellSx, borderRight: "1px solid", borderColor: "error.main"}) | |||||
}} | |||||
> | |||||
{percentFormatter.format( | |||||
Object.values(currentTaskGroups).reduce( | |||||
(acc, tg) => acc + tg.percentAllocation / 100, | |||||
0, | |||||
), | |||||
)} | |||||
</TableCell> | |||||
</TableRow> | |||||
</TableBody> | |||||
</Table> | |||||
</TableContainer> | |||||
</Box> | |||||
</Box> | |||||
); | |||||
}; | |||||
const NoTaskState: React.FC = () => { | |||||
const { t } = useTranslation(); | |||||
return ( | |||||
<> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Task Breakdown")} | |||||
</Typography> | |||||
<Alert severity="warning"> | |||||
{t('Please add some tasks first!')} | |||||
</Alert> | |||||
</> | |||||
); | |||||
}; | |||||
const ResourceAllocationWrapper: React.FC<Props> = (props) => { | |||||
const { getValues } = useFormContext<NewTaskTemplateFormInputs>(); | |||||
if (Object.keys(getValues("taskGroups")).length === 0) { | |||||
return <NoTaskState />; | |||||
} | |||||
return ( | |||||
<Stack spacing={4}> | |||||
<ResourceAllocationByGrade {...props} /> | |||||
<ResourceAllocationByStage {...props} /> | |||||
</Stack> | |||||
); | |||||
}; | |||||
export default ResourceAllocationWrapper; |
@@ -27,7 +27,7 @@ const TeamInfo: React.FC = ( | |||||
setValue, | setValue, | ||||
} = useFormContext<CreateTeamInputs>(); | } = useFormContext<CreateTeamInputs>(); | ||||
const resetCustomer = useCallback(() => { | |||||
const resetTeam = useCallback(() => { | |||||
console.log(defaultValues); | console.log(defaultValues); | ||||
if (defaultValues !== undefined) { | if (defaultValues !== undefined) { | ||||
resetField("description"); | resetField("description"); | ||||
@@ -199,20 +199,20 @@ const CustomerSave: React.FC<Props> = ({ | |||||
setServerError(""); | setServerError(""); | ||||
submitDialog(async () => { | submitDialog(async () => { | ||||
const response = await saveCustomer(data); | |||||
if (response.message === "Success") { | |||||
successDialog(t("Submit Success"), t).then(() => { | |||||
router.replace("/settings/customer"); | |||||
}) | |||||
} else { | |||||
errorDialog(t("Submit Fail"), t).then(() => { | |||||
formProps.setError("code", { message: response.message, type: "custom" }) | |||||
setTabIndex(0) | |||||
return false | |||||
}) | |||||
} | |||||
}, t) | |||||
const response = await saveCustomer(data); | |||||
if (response.message === "Success") { | |||||
successDialog(t("Submit Success"), t).then(() => { | |||||
router.replace("/settings/customer"); | |||||
}) | |||||
} else { | |||||
errorDialog(t("Submit Fail"), t).then(() => { | |||||
formProps.setError("code", { message: response.message, type: "custom" }) | |||||
setTabIndex(0) | |||||
return false | |||||
}) | |||||
} | |||||
}, t) | |||||
} catch (e) { | } catch (e) { | ||||
console.log(e) | console.log(e) | ||||
setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||