Переглянути джерело

Merge branch 'main' of https://git.2fi-solutions.com/wayne.lee/tsms

tags/Baseline_30082024_FRONTEND_UAT
MSI\2Fi 1 рік тому
джерело
коміт
8d86eae432
42 змінених файлів з 1573 додано та 323 видалено
  1. +213
    -70
      package-lock.json
  2. +28
    -0
      src/app/(main)/settings/skill/edit/page.tsx
  3. +17
    -0
      src/app/(main)/tasks/edit/not-found.tsx
  4. +24
    -3
      src/app/(main)/tasks/edit/page.tsx
  5. +2
    -2
      src/app/api/group/actions.ts
  6. +13
    -0
      src/app/api/tasks/actions.ts
  7. +12
    -0
      src/app/api/tasks/index.ts
  8. +2
    -1
      src/app/api/timesheets/actions.ts
  9. +10
    -5
      src/app/api/timesheets/utils.ts
  10. +5
    -3
      src/app/api/user/actions.ts
  11. +3
    -2
      src/app/api/user/index.ts
  12. +30
    -4
      src/components/CreateProject/CreateProject.tsx
  13. +31
    -2
      src/components/CreateProject/Milestone.tsx
  14. +29
    -17
      src/components/CreateProject/MilestoneSection.tsx
  15. +2
    -11
      src/components/CreateProject/ProjectTotalFee.tsx
  16. +21
    -18
      src/components/CreateProject/ResourceAllocation.tsx
  17. +1
    -1
      src/components/CreateProject/TaskSetup.tsx
  18. +42
    -38
      src/components/CreateTaskTemplate/CreateTaskTemplate.tsx
  19. +11
    -4
      src/components/CreateTaskTemplate/CreateTaskTemplateWrapper.tsx
  20. +151
    -0
      src/components/EditSkill/EditSkill.tsx
  21. +114
    -0
      src/components/EditSkill/EditSkillForm.tsx
  22. +40
    -0
      src/components/EditSkill/EditSkillLoading.tsx
  23. +21
    -0
      src/components/EditSkill/EditSkillWrapper.tsx
  24. +1
    -0
      src/components/EditSkill/index.ts
  25. +1
    -3
      src/components/EditTeam/Allocation.tsx
  26. +221
    -0
      src/components/EditUser/AuthAllocation.tsx
  27. +57
    -19
      src/components/EditUser/EditUser.tsx
  28. +3
    -3
      src/components/EditUser/EditUserWrapper.tsx
  29. +3
    -75
      src/components/EditUser/UserDetail.tsx
  30. +1
    -1
      src/components/EditUserGroup/EditUserGroup.tsx
  31. +0
    -2
      src/components/EditUserGroup/EditUserGroupWrapper.tsx
  32. +11
    -1
      src/components/LeaveModal/LeaveModal.tsx
  33. +152
    -0
      src/components/ProjectCashFlow/ProjectCashFlow.tsx
  34. +3
    -2
      src/components/SearchResults/SearchResults.tsx
  35. +17
    -15
      src/components/SkillSearch/SkillSearch.tsx
  36. +4
    -1
      src/components/StaffSearch/StaffSearch.tsx
  37. +2
    -10
      src/components/StaffSearch/StaffSearchWrapper.tsx
  38. +240
    -1
      src/components/StaffUtilization/StaffUtilization.tsx
  39. +7
    -3
      src/components/Swal/CustomAlerts.js
  40. +11
    -1
      src/components/TimesheetModal/TimesheetModal.tsx
  41. +13
    -4
      src/components/TimesheetTable/EntryInputTable.tsx
  42. +4
    -1
      src/components/TimesheetTable/TimesheetTable.tsx

+ 213
- 70
package-lock.json Переглянути файл

@@ -14,6 +14,7 @@
"@faker-js/faker": "^8.4.1",
"@fontsource/inter": "^5.0.16",
"@fontsource/plus-jakarta-sans": "^5.0.18",
"@fullcalendar/react": "^6.1.11",
"@mui/icons-material": "^5.15.0",
"@mui/material": "^5.15.0",
"@mui/material-nextjs": "^5.15.0",
@@ -21,13 +22,14 @@
"@mui/x-date-pickers": "^6.18.7",
"@unly/universal-language-detector": "^2.0.3",
"apexcharts": "^3.45.2",
"axios": "^1.6.8",
"date-holidays": "^3.23.11",
"dayjs": "^1.11.10",
"fullcalendar": "^6.1.11",
"i18next": "^23.7.11",
"i18next-resources-to-backend": "^1.2.0",
"lodash": "^4.17.21",
"next": "14.0.4",
"next-auth": "^4.24.5",
"next-auth": "^4.24.7",
"next-pwa": "^5.6.0",
"react": "^18",
"react-apexcharts": "^1.4.1",
@@ -2081,6 +2083,79 @@
"tslib": "^2.4.0"
}
},
"node_modules/@fullcalendar/core": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.11.tgz",
"integrity": "sha512-TjG7c8sUz+Vkui2FyCNJ+xqyu0nq653Ibe99A66LoW95oBo6tVhhKIaG1Wh0GVKymYiqAQN/OEdYTuj4ay27kA==",
"dependencies": {
"preact": "~10.12.1"
}
},
"node_modules/@fullcalendar/core/node_modules/preact": {
"version": "10.12.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/@fullcalendar/daygrid": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.11.tgz",
"integrity": "sha512-hF5jJB7cgUIxWD5MVjj8IU407HISyLu7BWXcEIuTytkfr8oolOXeCazqnnjmRbnFOncoJQVstTtq6SIhaT32Xg==",
"peerDependencies": {
"@fullcalendar/core": "~6.1.11"
}
},
"node_modules/@fullcalendar/interaction": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.11.tgz",
"integrity": "sha512-ynOKjzuPwEAMgTQ6R/Z2zvzIIqG4p8/Qmnhi1q0vzPZZxSIYx3rlZuvpEK2WGBZZ1XEafDOP/LGfbWoNZe+qdg==",
"peerDependencies": {
"@fullcalendar/core": "~6.1.11"
}
},
"node_modules/@fullcalendar/list": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.11.tgz",
"integrity": "sha512-9Qx8uvik9pXD12u50FiHwNzlHv4wkhfsr+r03ycahW7vEeIAKCsIZGTkUfFP+96I5wHihrfLazu1cFQG4MPiuw==",
"peerDependencies": {
"@fullcalendar/core": "~6.1.11"
}
},
"node_modules/@fullcalendar/multimonth": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/@fullcalendar/multimonth/-/multimonth-6.1.11.tgz",
"integrity": "sha512-7DbPC+AAlaKnquGVdw1Z85Q3nSZ4GZ1NcVIk4k7bLnqDlntwHPPsrDlSIzUWKcN0q5/u7jQHm4PU1m3LAl70Sg==",
"dependencies": {
"@fullcalendar/daygrid": "~6.1.11"
},
"peerDependencies": {
"@fullcalendar/core": "~6.1.11"
}
},
"node_modules/@fullcalendar/react": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.11.tgz",
"integrity": "sha512-Og0Tv0OiglTFp+b++yRyEhAeWnAmKkMLQ3iS0eJE1KDEov6QqGkoO+dUG4x8zp2w55IJqzik/a9iHi0s3oQDbA==",
"peerDependencies": {
"@fullcalendar/core": "~6.1.11",
"react": "^16.7.0 || ^17 || ^18",
"react-dom": "^16.7.0 || ^17 || ^18"
}
},
"node_modules/@fullcalendar/timegrid": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.11.tgz",
"integrity": "sha512-0seUHK/ferH89IeuCvV4Bib0zWjgK0nsptNdmAc9wDBxD/d9hm5Mdti0URJX6bDoRtsSfRDu5XsRcrzwoc+AUQ==",
"dependencies": {
"@fullcalendar/daygrid": "~6.1.11"
},
"peerDependencies": {
"@fullcalendar/core": "~6.1.11"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -3648,8 +3723,7 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/aria-query": {
"version": "5.3.0",
@@ -3855,6 +3929,14 @@
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
"dev": true
},
"node_modules/astronomia": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/astronomia/-/astronomia-4.1.1.tgz",
"integrity": "sha512-TcJD9lUC5eAo0/Ji7rnQauX/yQbi0yZWM+JsNr77W3OA5fsrgvuFgubLMFwfw4VlZ29cu9dG/yfJbfvuTSftjg==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/async": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
@@ -3869,11 +3951,6 @@
"has-symbols": "^1.0.3"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@@ -3942,16 +4019,6 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -4136,6 +4203,17 @@
"node": ">=10.16.0"
}
},
"node_modules/caldate": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/caldate/-/caldate-2.0.5.tgz",
"integrity": "sha512-JndhrUuDuE975KUhFqJaVR1OQkCHZqpOrJur/CFXEIEhWhBMjxO85cRSK8q4FW+B+yyPq6GYua2u4KvNzTcq0w==",
"dependencies": {
"moment-timezone": "^0.5.43"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@@ -4337,17 +4415,6 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -4489,6 +4556,68 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true
},
"node_modules/date-bengali-revised": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/date-bengali-revised/-/date-bengali-revised-2.0.2.tgz",
"integrity": "sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/date-chinese": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/date-chinese/-/date-chinese-2.1.4.tgz",
"integrity": "sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==",
"dependencies": {
"astronomia": "^4.1.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/date-easter": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/date-easter/-/date-easter-1.0.3.tgz",
"integrity": "sha512-aOViyIgpM4W0OWUiLqivznwTtuMlD/rdUWhc5IatYnplhPiWrLv75cnifaKYhmQwUBLAMWLNG4/9mlLIbXoGBQ==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/date-holidays": {
"version": "3.23.12",
"resolved": "https://registry.npmjs.org/date-holidays/-/date-holidays-3.23.12.tgz",
"integrity": "sha512-DLyP0PPVgNydgaTAY7SBS26+5h3KO1Z8FRKiAROkz0hAGNBLGAM48SMabfVa2ACRHH7Qw3LXYvlJkt9oa9WePA==",
"dependencies": {
"date-holidays-parser": "^3.4.4",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"prepin": "^1.0.3"
},
"bin": {
"holidays2json": "scripts/holidays2json.cjs"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/date-holidays-parser": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/date-holidays-parser/-/date-holidays-parser-3.4.4.tgz",
"integrity": "sha512-R5aO4oT8H51ZKdvApqHrqYEiNBrqT6tRj2PFXNcZfqMI4nxY7KKKly0ZsmquR5gY+x9ldKR8SAMdozzIInaoXg==",
"dependencies": {
"astronomia": "^4.1.1",
"caldate": "^2.0.5",
"date-bengali-revised": "^2.0.2",
"date-chinese": "^2.1.4",
"date-easter": "^1.0.2",
"deepmerge": "^4.3.1",
"jalaali-js": "^1.2.6",
"moment-timezone": "^0.5.43"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
@@ -4626,14 +4755,6 @@
"rimraf": "bin.js"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -5706,25 +5827,6 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -5802,6 +5904,19 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/fullcalendar": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-6.1.11.tgz",
"integrity": "sha512-OOlx/+yFn9k5LnucRzcDmShONBecOVKNN6HHWe8jl7hGzQBmkxO+iD6eBokO6p24EY1PjATqhZkhJqHiCUgx3A==",
"dependencies": {
"@fullcalendar/core": "~6.1.11",
"@fullcalendar/daygrid": "~6.1.11",
"@fullcalendar/interaction": "~6.1.11",
"@fullcalendar/list": "~6.1.11",
"@fullcalendar/multimonth": "~6.1.11",
"@fullcalendar/timegrid": "~6.1.11"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -6755,6 +6870,11 @@
"node": ">=8"
}
},
"node_modules/jalaali-js": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.6.tgz",
"integrity": "sha512-io974va+Qyu+UfuVX3UIAgJlxLhAMx9Y8VMfh+IG00Js7hXQo1qNQuwSiSa0xxco0SVgx5HWNkaiCcV+aZ8WPw=="
},
"node_modules/jest-worker": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
@@ -6800,9 +6920,9 @@
}
},
"node_modules/jose": {
"version": "4.15.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz",
"integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==",
"version": "4.15.5",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz",
"integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
@@ -6816,7 +6936,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -7135,6 +7254,7 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -7143,6 +7263,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -7183,6 +7304,25 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"engines": {
"node": "*"
}
},
"node_modules/moment-timezone": {
"version": "0.5.45",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz",
"integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==",
"dependencies": {
"moment": "^2.29.4"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -7275,14 +7415,14 @@
}
},
"node_modules/next-auth": {
"version": "4.24.6",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.6.tgz",
"integrity": "sha512-djQt3ZEaWEIxcsuh3HTW2uuzLfXMRjHH+ugAsichlQSbH4iA5MRcgMA2HvTNvsDTDLh44tyU72+/gWsxgTbAKg==",
"version": "4.24.7",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.7.tgz",
"integrity": "sha512-iChjE8ov/1K/z98gdKbn2Jw+2vLgJtVV39X+rCP5SGnVQuco7QOr19FRNGMIrD8d3LYhHWV9j9sKLzq1aDWWQQ==",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@panva/hkdf": "^1.0.2",
"cookie": "^0.5.0",
"jose": "^4.11.4",
"jose": "^4.15.5",
"oauth": "^0.9.15",
"openid-client": "^5.4.0",
"preact": "^10.6.3",
@@ -7993,6 +8133,14 @@
"node": ">= 0.8.0"
}
},
"node_modules/prepin": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/prepin/-/prepin-1.0.3.tgz",
"integrity": "sha512-0XL2hreherEEvUy0fiaGEfN/ioXFV+JpImqIzQjxk6iBg4jQ2ARKqvC4+BmRD8w/pnpD+lbxvh0Ub+z7yBEjvA==",
"bin": {
"prepin": "bin/prepin.js"
}
},
"node_modules/prettier": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
@@ -8057,11 +8205,6 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",


+ 28
- 0
src/app/(main)/settings/skill/edit/page.tsx Переглянути файл

@@ -0,0 +1,28 @@
import { Edit } from "@mui/icons-material";
import { useSearchParams } from "next/navigation";
// import EditStaff from "@/components/EditStaff";
import { Suspense } from "react";
import { I18nProvider, getServerI18n } from "@/i18n";
// import EditStaffWrapper from "@/components/EditStaff/EditStaffWrapper";
import { Metadata } from "next";
import EditSkill from "@/components/EditSkill";
import { Typography } from "@mui/material";


const EditSkillPage: React.FC = async () => {
const { t } = await getServerI18n("staff");

return (
<>
<Typography variant="h4">{t("Edit Skill")}</Typography>
<I18nProvider namespaces={["team", "common"]}>
<Suspense fallback={<EditSkill.Loading />}>
<EditSkill />
</Suspense>
</I18nProvider>
{/* <EditStaff /> */}
</>
);
};

export default EditSkillPage;

+ 17
- 0
src/app/(main)/tasks/edit/not-found.tsx Переглянути файл

@@ -0,0 +1,17 @@
import { getServerI18n } from "@/i18n";
import { Stack, Typography, Link } from "@mui/material";
import NextLink from "next/link";

export default async function NotFound() {
const { t } = await getServerI18n("tasks", "common");

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

+ 24
- 3
src/app/(main)/tasks/edit/page.tsx Переглянути файл

@@ -1,23 +1,44 @@
import { preloadAllTasks } from "@/app/api/tasks";
import { fetchTaskTemplate, preloadAllTasks } from "@/app/api/tasks";
import CreateTaskTemplate from "@/components/CreateTaskTemplate";
import { getServerI18n } from "@/i18n";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import { I18nProvider } from "@/i18n";
import { ServerFetchError } from "@/app/utils/fetchUtil";
import { isArray } from "lodash";
import { notFound } from "next/navigation";

export const metadata: Metadata = {
title: "Edit Task Template",
};

const TaskTemplates: React.FC = async () => {
interface Props {
searchParams: { [key: string]: string | string[] | undefined };
}

const TaskTemplates: React.FC<Props> = async ({ searchParams }) => {
const { t } = await getServerI18n("tasks");
const taskTemplateId = searchParams["id"];

if (!taskTemplateId || isArray(taskTemplateId)) {
notFound();
}

preloadAllTasks();

try {
await fetchTaskTemplate(taskTemplateId);
} catch (e) {
if (e instanceof ServerFetchError && e.response?.status === 404) {
notFound();
}
}

return (
<>
<Typography variant="h4">{t("Edit Task Template")}</Typography>
<I18nProvider namespaces={["tasks", "common"]}>
<CreateTaskTemplate />
<CreateTaskTemplate taskTemplateId={taskTemplateId}/>
</I18nProvider>
</>
);


+ 2
- 2
src/app/api/group/actions.ts Переглянути файл

@@ -29,8 +29,8 @@ export interface record {
records: auth[];
}

export const fetchAuth = cache(async (id?: number) => {
return serverFetchJson<record>(`${BASE_API_URL}/group/auth/combo/${id ?? 0}`, {
export const fetchAuth = cache(async (target: string, id?: number) => {
return serverFetchJson<record>(`${BASE_API_URL}/group/auth/${target}/${id ?? 0}`, {
next: { tags: ["auth"] },
});
});


+ 13
- 0
src/app/api/tasks/actions.ts Переглянути файл

@@ -4,13 +4,26 @@ import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil
import { BASE_API_URL } from "@/config/api";
import { TaskTemplate } from ".";
import { revalidateTag } from "next/cache";
import { ManhourAllocation } from "@/app/api/projects/actions";
import { Task, TaskGroup } from '@/app/api/tasks';

export interface NewTaskTemplateFormInputs {

// task template
code: string;
name: string;
taskIds: number[];

id: number | null;

// resource allocation template
manhourPercentageByGrade: ManhourAllocation;
taskGroups: {
[taskGroup: TaskGroup["id"]]: {
taskIds: Task["id"][];
percentAllocation: number;
};
};
}

export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => {


+ 12
- 0
src/app/api/tasks/index.ts Переглянути файл

@@ -39,3 +39,15 @@ export const preloadAllTasks = () => {
export const fetchAllTasks = cache(async () => {
return serverFetchJson<Task[]>(`${BASE_API_URL}/tasks`);
});

export const fetchTaskTemplate = cache(async (id: string) => {
const taskTemplate = await serverFetchJson<TaskTemplate>(
`${BASE_API_URL}/tasks/templatesDetails/${id}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);

return taskTemplate;
});

+ 2
- 1
src/app/api/timesheets/actions.ts Переглянути файл

@@ -11,7 +11,8 @@ export interface TimeEntry {
projectId?: ProjectResult["id"];
taskGroupId?: TaskGroup["id"];
taskId?: Task["id"];
inputHours: number;
inputHours?: number;
otHours?: number;
remark?: string;
}



+ 10
- 5
src/app/api/timesheets/utils.ts Переглянути файл

@@ -8,19 +8,24 @@ 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.inputHours || !(entry.inputHours >= 0)) {
error = "inputHours";
}
} else {
if (!entry.inputHours || !(entry.inputHours >= 0)) {
error = "inputHours";
} else if (!entry.remark) {
if (!entry.remark) {
error = "remark";
}
}


+ 5
- 3
src/app/api/user/actions.ts Переглянути файл

@@ -7,8 +7,10 @@ import { UserDetail, UserResult } from ".";
import { cache } from "react";

export interface UserInputs {
username: string;
email: string;
name: string;
email?: string;
addAuthIds?: number[];
removeAuthIds?: number[];
}

export interface PasswordInputs {
@@ -40,7 +42,7 @@ export const deleteUser = async (id: number) => {
};

export const changePassword = async (data: any) => {
return serverFetchJson(`${BASE_API_URL}/user/change-password`, {
return serverFetchWithNoContent(`${BASE_API_URL}/user/change-password`, {
method: "PATCH",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },


+ 3
- 2
src/app/api/user/index.ts Переглянути файл

@@ -3,7 +3,6 @@ import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import "server-only";


export interface UserResult {
action: any;
id: number;
@@ -20,6 +19,7 @@ export interface UserResult {
phone2: string;
remarks: string;
groupId: number;
auths: any
}

// export interface DetailedUser extends UserResult {
@@ -28,9 +28,10 @@ export interface UserResult {
// }

export interface UserDetail {
authIds: number[];
data: UserResult;
authIds: number[];
groupIds: number[];
auths: any[]
}

export const preloadUser = () => {


+ 30
- 4
src/components/CreateProject/CreateProject.tsx Переглянути файл

@@ -144,22 +144,48 @@ const CreateProject: React.FC<Props> = ({

// detect errors
let hasErrors = false

// Tab - Staff Allocation and Resource
if (data.totalManhour === null || data.totalManhour <= 0) {
formProps.setError("totalManhour", { message: "totalManhour value is not valid", type: "required" })
setTabIndex(2)
hasErrors = true
}

const manhourPercentageByGradeKeys = Object.keys(data.manhourPercentageByGrade)
if (manhourPercentageByGradeKeys.filter(k => data.manhourPercentageByGrade[k as any] < 0).length > 0 ||
manhourPercentageByGradeKeys.reduce((acc, value) => acc + data.manhourPercentageByGrade[value as any], 0) !== 1) {
manhourPercentageByGradeKeys.reduce((acc, value) => acc + data.manhourPercentageByGrade[value as any], 0) !== 100) {
formProps.setError("manhourPercentageByGrade", { message: "manhourPercentageByGrade value is not valid", type: "invalid" })
setTabIndex(2)
hasErrors = true
}

const taskGroupKeys = Object.keys(data.taskGroups)
if (taskGroupKeys.filter(k => data.taskGroups[k as any].percentAllocation < 0).length > 0 ||
taskGroupKeys.reduce((acc, value) => acc + data.taskGroups[value as any].percentAllocation, 0) !== 1) {
formProps.setError("taskGroups", {message: "Task Groups value is not invalid", type: "invalid"})
taskGroupKeys.reduce((acc, value) => acc + data.taskGroups[value as any].percentAllocation, 0) !== 100) {
formProps.setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" })
setTabIndex(2)
hasErrors = true
}

// Tab - Milestone
let projectTotal = 0
const milestonesKeys = Object.keys(data.milestones)
milestonesKeys.forEach(key => {
const { startDate, endDate, payments } = data.milestones[parseFloat(key)]

if (!Boolean(startDate) || startDate === "Invalid Date" || !Boolean(endDate) || endDate === "Invalid Date" || new Date(startDate) > new Date(endDate)) {
formProps.setError("milestones", {message: "milestones is not valid", type: "invalid"})
setTabIndex(3)
hasErrors = true
}

projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0)
})

if (projectTotal !== data.expectedProjectFee) {
formProps.setError("milestones", {message: "milestones is not valid", type: "invalid"})
setTabIndex(3)
hasErrors = true
}

@@ -245,7 +271,7 @@ const CreateProject: React.FC<Props> = ({
// manhourPercentageByGrade should have a sensible default
manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade)
? grades.reduce((acc, grade) => {
return { ...acc, [grade.id]: 1 / grades.length };
return { ...acc, [grade.id]: 100 / grades.length };
}, {})
: defaultInputs?.manhourPercentageByGrade,
},


+ 31
- 2
src/components/CreateProject/Milestone.tsx Переглянути файл

@@ -4,7 +4,7 @@ import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import { useTranslation } from "react-i18next";
import Button from "@mui/material/Button";
import React, { useCallback, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt";
import {
@@ -29,7 +29,7 @@ export interface Props {

const Milestone: React.FC<Props> = ({ allTasks, isActive }) => {
const { t } = useTranslation();
const { watch } = useFormContext<CreateProjectInputs>();
const { watch, setError, clearErrors } = useFormContext<CreateProjectInputs>();
const currentTaskGroups = watch("taskGroups");
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.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 (
<>
<Card sx={{ display: isActive ? "block" : "none" }}>


+ 29
- 17
src/components/CreateProject/MilestoneSection.tsx Переглянути файл

@@ -26,7 +26,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs from "dayjs";
import "dayjs/locale/zh-hk";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useFormContext } from "react-hook-form";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { INPUT_DATE_FORMAT, moneyFormatter } from "@/app/utils/formatUtil";
@@ -57,7 +57,9 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {

const apiRef = useGridApiRef();
const addRow = useCallback(() => {
const id = Date.now();
// const id = Date.now();
const minId = Math.min(...payments.map((payment) => payment.id!!));
const id = minId >= 0 ? -1 : minId - 1
setPayments((p) => [...p, { id, _isNew: true }]);
setRowModesModel((model) => ({
...model,
@@ -239,21 +241,26 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs>
<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>
</Grid>
<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>
</Grid>


+ 2
- 11
src/components/CreateProject/ProjectTotalFee.tsx Переглянути файл

@@ -2,7 +2,7 @@ import { CreateProjectInputs } from "@/app/api/projects/actions";
import { TaskGroup } from "@/app/api/tasks";
import { moneyFormatter } from "@/app/utils/formatUtil";
import { Divider, Stack, Typography } from "@mui/material";
import React, { useCallback, useEffect, useMemo } from "react";
import React from "react";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";

@@ -12,21 +12,12 @@ interface Props {

const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => {
const { t } = useTranslation();
const { watch, setError, clearErrors } = useFormContext<CreateProjectInputs>();
const { watch } = useFormContext<CreateProjectInputs>();
const milestones = watch("milestones");
const expectedTotalFee = watch("expectedProjectFee");

let projectTotal = 0;

useEffect(() => {
console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseInt(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0))
if (Object.keys(milestones).reduce((acc, key) => acc + milestones[parseInt(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0) !== expectedTotalFee) {
setError("milestones", {message: "project total is not valid", type: "invalid"})
} else {
clearErrors("milestones")
}
}, [milestones])

return (
<Stack spacing={1}>
{taskGroups.map((group, index) => {


+ 21
- 18
src/components/CreateProject/ResourceAllocation.tsx Переглянути файл

@@ -62,10 +62,10 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {

const manhourPercentageByGrade = watch("manhourPercentageByGrade");
const totalManhour = watch("totalManhour");
const totalPercentage = Object.values(manhourPercentageByGrade).reduce(
const totalPercentage = Math.round(Object.values(manhourPercentageByGrade).reduce(
(acc, percent) => acc + percent,
0,
);
) * 100) / 100;

const makeUpdatePercentage = useCallback(
(gradeId: Grade["id"]) => (percentage?: number) => {
@@ -78,7 +78,7 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {

const keys = Object.keys(updatedManhourPercentageByGrade)
if (keys.filter(k => updatedManhourPercentageByGrade[k as any] < 0).length > 0 ||
keys.reduce((acc, value) => acc + updatedManhourPercentageByGrade[value as any], 0) !== 1) {
keys.reduce((acc, value) => acc + updatedManhourPercentageByGrade[value as any], 0) !== 100) {
setError("manhourPercentageByGrade", {message: "manhourPercentageByGrade value is not valid", type: "invalid"})
} else {
clearErrors("manhourPercentageByGrade")
@@ -134,7 +134,8 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {
<TableCellEdit
key={`${column.id}${idx}`}
value={manhourPercentageByGrade[column.id]}
renderValue={(val) => percentFormatter.format(val)}
renderValue={(val) => val + "%"}
// renderValue={(val) => percentFormatter.format(val)}
onChange={makeUpdatePercentage(column.id)}
convertValue={(inputValue) => Number(inputValue)}
cellSx={{ backgroundColor: "primary.lightest" }}
@@ -142,8 +143,9 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {
error={manhourPercentageByGrade[column.id] < 0}
/>
))}
<TableCell sx={{ ...(totalPercentage === 1 && leftBorderCellSx), ...(totalPercentage !== 1 && {...errorCellSx, borderRight: "1px solid", borderColor: "error.main"})}}>
{percentFormatter.format(totalPercentage)}
<TableCell sx={{ ...(totalPercentage === 100 && leftBorderCellSx), ...(totalPercentage !== 100 && {...errorCellSx, borderRight: "1px solid", borderColor: "error.main"})}}>
{totalPercentage + "%"}
{/* {percentFormatter.format(totalPercentage)} */}
</TableCell>
</TableRow>
<TableRow>
@@ -151,7 +153,7 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {
{grades.map((column, idx) => (
<TableCell key={`${column.id}${idx}`}>
{manhourFormatter.format(
manhourPercentageByGrade[column.id] * totalManhour,
manhourPercentageByGrade[column.id] / 100 * totalManhour,
)}
</TableCell>
))}
@@ -203,7 +205,7 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {

const keys = Object.keys(updatedTaskGroups)
if (keys.filter(k => updatedTaskGroups[k as any].percentAllocation < 0).length > 0 ||
keys.reduce((acc, value) => acc + updatedTaskGroups[value as any].percentAllocation, 0) !== 1) {
keys.reduce((acc, value) => acc + updatedTaskGroups[value as any].percentAllocation, 0) !== 100) {
setError("taskGroups", {message: "Task Groups value is not invalid", type: "invalid"})
} else {
clearErrors("taskGroups")
@@ -250,7 +252,8 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
</TableCell>
<TableCellEdit
value={currentTaskGroups[tg.id].percentAllocation}
renderValue={(val) => percentFormatter.format(val)}
// renderValue={(val) => percentFormatter.format(val)}
renderValue={(val) => val + "%"}
onChange={makeUpdatePercentage(tg.id)}
convertValue={(inputValue) => Number(inputValue)}
cellSx={{
@@ -261,16 +264,16 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
/>
<TableCell sx={rightBorderCellSx}>
{manhourFormatter.format(
currentTaskGroups[tg.id].percentAllocation * totalManhour,
currentTaskGroups[tg.id].percentAllocation / 100 * totalManhour,
)}
</TableCell>
{grades.map((column, idx) => {
const stageHours =
currentTaskGroups[tg.id].percentAllocation * totalManhour;
currentTaskGroups[tg.id].percentAllocation / 100 * totalManhour;
return (
<TableCell key={`${column.id}${idx}`}>
{manhourFormatter.format(
manhourPercentageByGrade[column.id] * stageHours,
manhourPercentageByGrade[column.id] / 100 * stageHours,
)}
</TableCell>
);
@@ -286,13 +289,13 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
)}
</TableCell>
<TableCell sx={{
...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) === 1 && leftBorderCellSx),
...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) !== 1 && errorCellSx)
...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) === 100 && leftBorderCellSx),
...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) !== 100 && errorCellSx)
}}
>
{percentFormatter.format(
Object.values(currentTaskGroups).reduce(
(acc, tg) => acc + tg.percentAllocation,
(acc, tg) => acc + tg.percentAllocation / 100,
0,
),
)}
@@ -300,7 +303,7 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
<TableCell sx={rightBorderCellSx}>
{manhourFormatter.format(
Object.values(currentTaskGroups).reduce(
(acc, tg) => acc + tg.percentAllocation * totalManhour,
(acc, tg) => acc + tg.percentAllocation / 100 * totalManhour,
0,
),
)}
@@ -309,9 +312,9 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
const hours = Object.values(currentTaskGroups).reduce(
(acc, tg) =>
acc +
tg.percentAllocation *
tg.percentAllocation / 100 *
totalManhour *
manhourPercentageByGrade[column.id],
manhourPercentageByGrade[column.id] / 100 ,
0,
);
return (


+ 1
- 1
src/components/CreateProject/TaskSetup.tsx Переглянути файл

@@ -52,7 +52,7 @@ const TaskSetup: React.FC<Props> = ({
(e: SelectChangeEvent<number | "All">) => {
if (e.target.value === "All" || isNumber(e.target.value)) {
setSelectedTaskTemplateId(e.target.value);
onReset();
// onReset();
}
},
[onReset],


+ 42
- 38
src/components/CreateTaskTemplate/CreateTaskTemplate.tsx Переглянути файл

@@ -13,7 +13,7 @@ import Close from "@mui/icons-material/Close";
import { useRouter, useSearchParams } from "next/navigation";
import React from "react";
import Stack from "@mui/material/Stack";
import { Task } from "@/app/api/tasks";
import { Task, TaskTemplate } from "@/app/api/tasks";
import {
NewTaskTemplateFormInputs,
fetchTaskTemplate,
@@ -24,9 +24,10 @@ import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts";

interface Props {
tasks: Task[];
defaultInputs?: TaskTemplate;
}

const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => {
const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => {
const { t } = useTranslation();

const searchParams = useSearchParams()
@@ -54,45 +55,45 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => {
watch,
resetField,
formState: { errors, isSubmitting },
} = useForm<NewTaskTemplateFormInputs>({ defaultValues: { taskIds: [] } });
} = useForm<NewTaskTemplateFormInputs>({ defaultValues: defaultInputs });

const currentTaskIds = watch("taskIds");
const selectedItems = React.useMemo(() => {
return items.filter((item) => currentTaskIds.includes(item.id));
}, [currentTaskIds, items]);

const [refTaskTemplate, setRefTaskTemplate] = React.useState<NewTaskTemplateFormInputs>()
const id = searchParams.get('id')
const fetchCurrentTaskTemplate = async () => {
try {
const taskTemplate = await fetchTaskTemplate(parseInt(id!!))
const defaultValues = {
id: parseInt(id!!),
code: taskTemplate.code ?? null,
name: taskTemplate.name ?? null,
taskIds: taskTemplate.tasks.map(task => task.id) ?? [],
}
setRefTaskTemplate(defaultValues)
} catch (e) {
console.log(e)
}
}
React.useLayoutEffect(() => {
if (id !== null && parseInt(id) > 0) fetchCurrentTaskTemplate()
}, [id])
React.useEffect(() => {
if (refTaskTemplate) {
setValue("taskIds", refTaskTemplate.taskIds)
resetField("code", { defaultValue: refTaskTemplate.code })
resetField("name", { defaultValue: refTaskTemplate.name })
setValue("id", refTaskTemplate.id)
}
}, [refTaskTemplate])
// const [refTaskTemplate, setRefTaskTemplate] = React.useState<NewTaskTemplateFormInputs>()
// const id = searchParams.get('id')
// const fetchCurrentTaskTemplate = async () => {
// try {
// const taskTemplate = await fetchTaskTemplate(parseInt(id!!))
// const defaultValues = {
// id: parseInt(id!!),
// code: taskTemplate.code ?? null,
// name: taskTemplate.name ?? null,
// taskIds: taskTemplate.tasks.map(task => task.id) ?? [],
// }
// setRefTaskTemplate(defaultValues)
// } catch (e) {
// console.log(e)
// }
// }
// React.useLayoutEffect(() => {
// if (id !== null && parseInt(id) > 0) fetchCurrentTaskTemplate()
// }, [id])
// React.useEffect(() => {
// if (refTaskTemplate) {
// setValue("taskIds", refTaskTemplate.taskIds)
// resetField("code", { defaultValue: refTaskTemplate.code })
// resetField("name", { defaultValue: refTaskTemplate.name })
// setValue("id", refTaskTemplate.id)
// }
// }, [refTaskTemplate])

const onSubmit: SubmitHandler<NewTaskTemplateFormInputs> = React.useCallback(
async (data) => {
@@ -120,9 +121,9 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => {

return (
<>
{
(id === null || refTaskTemplate !== undefined) && <Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}>
<Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}>
<Card>
{/* Task List Setup */}
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Task List Setup")}</Typography>
<Grid
@@ -166,6 +167,9 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => {
allItemsLabel={t("Task Pool")}
selectedItemsLabel={t("Task List Template")}
/>
{/* Task List Setup */}
{/* Task List Setup */}

</CardContent>
</Card>
{
@@ -188,7 +192,7 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => {
{t("Confirm")}
</Button>
</Stack>
</Stack >}
</Stack >
</>
);
};


+ 11
- 4
src/components/CreateTaskTemplate/CreateTaskTemplateWrapper.tsx Переглянути файл

@@ -1,11 +1,18 @@
import React from "react";
import CreateTaskTemplate from "./CreateTaskTemplate";
import { fetchAllTasks } from "@/app/api/tasks";
import { fetchAllTasks, fetchTaskTemplate } from "@/app/api/tasks";

const CreateTaskTemplateWrapper: React.FC = async () => {
const tasks = await fetchAllTasks();
interface Props {
taskTemplateId?: string;
}

return <CreateTaskTemplate tasks={tasks} />;
const CreateTaskTemplateWrapper: React.FC<Props> = async (props) => {
const [tasks] = await Promise.all([
fetchAllTasks(),
]);

const taskTemplateInfo = props.taskTemplateId ? await fetchTaskTemplate(props.taskTemplateId) : undefined
return <CreateTaskTemplate tasks={tasks} defaultInputs={taskTemplateInfo}/>;
};

export default CreateTaskTemplateWrapper;

+ 151
- 0
src/components/EditSkill/EditSkill.tsx Переглянути файл

@@ -0,0 +1,151 @@
"use client";
import { SkillResult } from "@/app/api/skill";
import {
Button,
Card,
CardContent,
Grid,
Stack,
Tab,
Tabs,
TabsProps,
TextField,
Typography,
} from "@mui/material";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
useFormContext,
} from "react-hook-form";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter, useSearchParams } from "next/navigation";
import { Check, Close, Error, RestartAlt } from "@mui/icons-material";
import EditSkillForm from "./EditSkillForm";
import { CreateSkillInputs, saveSkill } from "@/app/api/skill/actions";
import AuthAllocation from "../EditUser/AuthAllocation";

interface Props {
skills: SkillResult[];
}

const EditSkill: React.FC<Props> = async ({ skills }) => {
const { t } = useTranslation();
const formProps = useForm<CreateSkillInputs>();
const [serverError, setServerError] = useState("");
const router = useRouter();
const searchParams = useSearchParams();
const id = parseInt(searchParams.get("id") || "0");
const [tabIndex, setTabIndex] = useState(0);
const [filteredSkill, setFilteredSkill] = useState<SkillResult>(() =>
skills.filter((s) => s.id === id)[0] as SkillResult
);
const errors = formProps.formState.errors;

const onSubmit = useCallback<SubmitHandler<CreateSkillInputs>>(
async (data) => {
try {
console.log(data);
const postData = {
...data,
id: id
}
await saveSkill(postData)
router.replace(`/settings/skill`)
} catch (e) {
console.log(e);
setServerError(t("An error has occurred. Please try again later."));
}
},
[router]
);

const handleCancel = () => {
router.back();
};

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);

useEffect(() => {
formProps.reset({
name: filteredSkill.name,
code: filteredSkill.code,
description: filteredSkill.description
});
}, [skills]);

const hasErrorsInTab = (
tabIndex: number,
errors: FieldErrors<CreateSkillInputs>
) => {
switch (tabIndex) {
case 0:
return Object.keys(errors).length > 0;
default:
false;
}
};

return (
<>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab
label={t("Skill Info")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end"
/>
{/* <Tab label={t("Certification")} iconPosition="end" /> */}
</Tabs>
{tabIndex === 0 && <EditSkillForm />}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="text"
startIcon={<RestartAlt />}
// onClick={() => console.log("asdasd")}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit">
{t("Confirm")}
</Button>
</Stack>
</Stack>
</FormProvider>
</>
);
};
export default EditSkill;

+ 114
- 0
src/components/EditSkill/EditSkillForm.tsx Переглянути файл

@@ -0,0 +1,114 @@
"use client";

import { CreateSkillInputs } from "@/app/api/skill/actions";
import {
Box,
Button,
Card,
CardContent,
Grid,
Stack,
Tab,
Tabs,
TabsProps,
TextField,
Typography,
} from "@mui/material";
import { useSearchParams } from "next/navigation";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
useFormContext,
} from "react-hook-form";
import { useTranslation } from "react-i18next";

interface Props {
// users: UserResult[]
}

const EditSkillForm: React.FC<Props> = async ({}) => {
const { t } = useTranslation();
const searchParams = useSearchParams();
const idString = searchParams.get("id");
const {
register,
setValue,
getValues,
formState: { errors, defaultValues },
reset,
resetField,
} = useFormContext<CreateSkillInputs>();
// const formProps = useForm({});

return (
<>
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Skill Info")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Skill Name")}
fullWidth
rows={4}
{...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={6}>
<TextField
label={t("Skill Code")}
fullWidth
rows={4}
{...register("code", {
required: true,
})}
error={Boolean(errors.code)}
helperText={
Boolean(errors.code) &&
(errors.code?.message
? t(errors.code.message)
: t("Please input correct name"))
}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("Skill Description")}
fullWidth
multiline
rows={4}
{...register("description", {
required: true,
})}
error={Boolean(errors.description)}
helperText={
Boolean(errors.description) &&
(errors.description?.message
? t(errors.description.message)
: t("Please input correct name"))
}
/>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>
</>
);
};
export default EditSkillForm;

+ 40
- 0
src/components/EditSkill/EditSkillLoading.tsx Переглянути файл

@@ -0,0 +1,40 @@
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const EditSkillLoading: React.FC = () => {
return (
<>
<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>Edit Skill
<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 EditSkillLoading;

+ 21
- 0
src/components/EditSkill/EditSkillWrapper.tsx Переглянути файл

@@ -0,0 +1,21 @@
import React from "react";
import EditSkill from "./EditSkill";
import EditSkillLoading from "./EditSkillLoading";
import { fetchStaff, fetchTeamLeads } from "@/app/api/staff";
import { useSearchParams } from "next/navigation";
import { fetchSkill } from "@/app/api/skill";

interface SubComponents {
Loading: typeof EditSkillLoading;
}

const EditSkillWrapper: React.FC & SubComponents = async () => {
const skills = await fetchSkill()
console.log(skills)

return <EditSkill skills={skills}/>;
};

EditSkillWrapper.Loading = EditSkillLoading;

export default EditSkillWrapper;

+ 1
- 0
src/components/EditSkill/index.ts Переглянути файл

@@ -0,0 +1 @@
export { default } from "./EditSkillWrapper";

+ 1
- 3
src/components/EditTeam/Allocation.tsx Переглянути файл

@@ -16,8 +16,8 @@ import { Staff4TransferList, fetchStaffCombo } from "@/app/api/staff/actions";
import { StaffResult, StaffTeamTable } from "@/app/api/staff";
import SearchResults, { Column } from "../SearchResults";
import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material";
import { Card } from "reactstrap";
import {
Card,
Box,
CardContent,
Grid,
@@ -49,8 +49,6 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => {
reset,
resetField,
} = useFormContext<CreateTeamInputs>();
// let firstFilter: StaffResult[] = []

const initialStaffs = staff.map((s) => ({ ...s }));
const [filteredStaff, setFilteredStaff] = useState(initialStaffs);


+ 221
- 0
src/components/EditUser/AuthAllocation.tsx Переглянути файл

@@ -0,0 +1,221 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Add, Clear, PersonAdd, PersonRemove, Remove, Search } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
useFormContext,
} from "react-hook-form";
import {
Box,
Card,
CardContent,
Grid,
IconButton,
InputAdornment,
Stack,
Tab,
Tabs,
TabsProps,
TextField,
Typography,
} from "@mui/material";
import { differenceBy } from "lodash";
import { UserInputs } from "@/app/api/user/actions";
import { auth } from "@/app/api/group/actions";
import SearchResults, { Column } from "../SearchResults";

export interface Props {
auths: auth[]
}

const AuthAllocation: React.FC<Props> = ({ auths }) => {
const { t } = useTranslation();
const searchParams = useSearchParams();
const id = parseInt(searchParams.get("id") || "0");
const {
setValue,
getValues,
formState: { defaultValues },
reset,
resetField,
} = useFormContext<UserInputs>();
const initialAuths = auths.map((u) => ({ ...u })).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)
);
}
);
const [removeAuthIds, setRemoveAuthIds] = useState<number[]>([]);

// Adding / Removing Auth
const addAuth = useCallback((auth: auth) => {
setSelectedAuths((a) => [...a, auth]);
}, []);
const removeAuth = useCallback((auth: auth) => {
setSelectedAuths((a) => a.filter((a) => a.id !== auth.id));
setRemoveAuthIds((prevIds) => [...prevIds, auth.id]);
}, []);

const clearAuth = useCallback(() => {
if (defaultValues !== undefined) {
resetField("addAuthIds");
setSelectedAuths(
initialAuths.filter((auth) => defaultValues.addAuthIds?.includes(auth.id))
);
}
}, [defaultValues]);

// Sync with form
useEffect(() => {
setValue(
"addAuthIds",
selectedAuths.map((a) => a.id)
);
setValue(
"removeAuthIds",
removeAuthIds
);
}, [selectedAuths, removeAuthIds, setValue]);

const AuthPoolColumns = useMemo<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))
// })
// );
}, [auths, query]);

const resetAuth = React.useCallback(() => {
clearQueryInput();
clearAuth();
}, [clearQueryInput, clearAuth]);

const formProps = useForm({});

// Tab related
const [tabIndex, setTabIndex] = React.useState(0);
const handleTabChange = React.useCallback<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 AuthAllocation

+ 57
- 19
src/components/EditUser/EditUser.tsx Переглянути файл

@@ -1,6 +1,6 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react";
import SearchResults, { Column } from "../SearchResults";
// import { TeamResult } from "@/app/api/team";
import { useTranslation } from "react-i18next";
@@ -26,9 +26,11 @@ import {
} from "react-hook-form";
import { Check, Close, Error, RestartAlt } from "@mui/icons-material";
import { StaffResult } from "@/app/api/staff";
import { editUser, fetchUserDetails } from "@/app/api/user/actions";
import { UserInputs, editUser, fetchUserDetails } from "@/app/api/user/actions";
import UserDetail from "./UserDetail";
import { UserResult } from "@/app/api/user";
import { auth, fetchAuth } from "@/app/api/group/actions";
import AuthAllocation from "./AuthAllocation";

interface Props {
// users: UserResult[]
@@ -36,11 +38,14 @@ interface Props {

const EditUser: React.FC<Props> = async ({ }) => {
const { t } = useTranslation();
const formProps = useForm<UserResult>();
const formProps = useForm<UserInputs>();
const searchParams = useSearchParams();
const id = parseInt(searchParams.get("id") || "0");
const [tabIndex, setTabIndex] = useState(0);
const router = useRouter();
const [serverError, setServerError] = useState("");
const [data, setData] = useState<UserResult>();
const [auths, setAuths] = useState<auth[]>();

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
@@ -49,38 +54,45 @@ const EditUser: React.FC<Props> = async ({ }) => {
[]
);

const [serverError, setServerError] = useState("");
const [data, setData] = useState<UserResult>();
const errors = formProps.formState.errors;

const fetchUserDetail = async () => {
console.log(id);
try {
// fetch user info
const userDetail = await fetchUserDetails(id);
console.log(userDetail);
const _data = userDetail.data as UserResult;
console.log(_data);
setData(_data);
//fetch user auths
const authDetail = await fetchAuth("user", id);
setAuths(authDetail.records)
const addAuthIds = authDetail.records.filter((item) => item.v === 1).map((item) => item.id).sort((a, b) => a - b);

formProps.reset({
username: _data.username,
firstname: _data.firstname,
lastname: _data.lastname,
title: _data.title,
department: _data.department,
name: _data.username,
email: _data.email,
phone1: _data.phone1,
phone2: _data.phone2,
remarks: _data.remarks,
addAuthIds: addAuthIds || []
});
} catch (error) {
console.log(error);
setServerError(t("An error has occurred. Please try again later."));
}
};
}

useEffect(() => {
fetchUserDetail();
}, []);

// useEffect(() => {
// const thisUser = users.filter((item) => item.id === id)
// formProps.reset({
// username: thisUser[0].username,
// email: thisUser[0].email,
// });
// }, []);

const hasErrorsInTab = (
tabIndex: number,
errors: FieldErrors<UserResult>
@@ -97,14 +109,16 @@ const EditUser: React.FC<Props> = async ({ }) => {
router.back();
};

const onSubmit = useCallback<SubmitHandler<UserResult>>(
const onSubmit = useCallback<SubmitHandler<UserInputs>>(
async (data) => {
try {
console.log(data);
const tempData = {
username: data.username,
name: data.name,
email: data.email,
locked: false
locked: false,
addAuthIds: data.addAuthIds || [],
removeAuthIds: data.removeAuthIds || [],
}
console.log(tempData);
await editUser(id, tempData);
@@ -116,7 +130,7 @@ const EditUser: React.FC<Props> = async ({ }) => {
},
[router]
);
const onSubmitError = useCallback<SubmitErrorHandler<UserResult>>(
const onSubmitError = useCallback<SubmitErrorHandler<UserInputs>>(
(errors) => {
console.log(errors);
},
@@ -136,7 +150,31 @@ const EditUser: React.FC<Props> = async ({ }) => {
component="form"
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
<UserDetail data={data!!} />
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab
label={t("User Detail")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end"
/>
<Tab label={t("User Authority")} iconPosition="end" />
</Tabs>
</Stack>
{tabIndex == 0 && <UserDetail data={data!} />}
{tabIndex === 1 && <AuthAllocation auths={auths!}/>}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="text"


+ 3
- 3
src/components/EditUser/EditUserWrapper.tsx Переглянути файл

@@ -12,10 +12,10 @@ interface SubComponents {
}

const EditUserWrapper: React.FC & SubComponents = async () => {
// const users = await fetchUser()
// console.log(users)
const users = await fetchUser()
console.log(users)

return <EditUser />;
return <EditUser />
};

EditUserWrapper.Loading = EditUserLoading;


+ 3
- 75
src/components/EditUser/UserDetail.tsx Переглянути файл

@@ -1,6 +1,7 @@
"use client";

import { UserResult } from "@/app/api/user";
import { UserInputs } from "@/app/api/user/actions";
import {
Card,
CardContent,
@@ -25,7 +26,7 @@ const UserDetail: React.FC<Props> = ({
register,
formState: { errors },
control,
} = useFormContext<UserResult>();
} = useFormContext<UserInputs>();

return (
<Card>
@@ -38,52 +39,12 @@ const UserDetail: React.FC<Props> = ({
<TextField
label={t("username")}
fullWidth
{...register("username", {
{...register("name", {
required: "username required!",
})}
error={Boolean(errors.name)}
/>
</Grid>
{/* <Grid item xs={6}>
<TextField
label={t("First Name")}
fullWidth
{...register("firstname", {
required: "Name required!",
})}
error={Boolean(errors.firstname)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Last Name")}
fullWidth
{...register("lastname", {
required: "Name required!",
})}
error={Boolean(errors.lastname)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("title")}
fullWidth
{...register("title", {
required: "title required!",
})}
error={Boolean(errors.title)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("department")}
fullWidth
{...register("department", {
required: "department required!",
})}
error={Boolean(errors.department)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("email")}
@@ -94,39 +55,6 @@ const UserDetail: React.FC<Props> = ({
error={Boolean(errors.email)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("phone1")}
fullWidth
{...register("phone1", {
required: "phone1 required!",
})}
error={Boolean(errors.phone1)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("phone2")}
fullWidth
{...register("phone2", {
required: "phone2 required!",
})}
error={Boolean(errors.phone2)}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("remarks")}
fullWidth
multiline
rows={4}
variant="filled"
{...register("remarks", {
required: "remarks required!",
})}
error={Boolean(errors.remarks)}
/>
</Grid> */}
</Grid>
</CardContent>
</Card>


+ 1
- 1
src/components/EditUserGroup/EditUserGroup.tsx Переглянути файл

@@ -83,7 +83,7 @@ const EditUserGroup: React.FC<Props> = ({ groups, users }) => {
const thisGroup = groups.filter((item) => item.id === id)[0];
const addUserIds = users.filter((item) => item.groupId === id).map((data) => data.id)
let addAuthIds: number[] = []
fetchAuth(id).then((data) => {
fetchAuth("group", id).then((data) => {
setAuths(data.records)
addAuthIds = data.records.filter((data) => data.v === 1).map((data) => data.id).sort((a, b) => a - b);
formProps.reset({


+ 0
- 2
src/components/EditUserGroup/EditUserGroupWrapper.tsx Переглянути файл

@@ -2,9 +2,7 @@ import React from "react";
import EditUserGroup from "./EditUserGroup";
import EditUserGroupLoading from "./EditUserGroupLoading";
import { fetchGroup } from "@/app/api/group";
import { fetchAuth } from "@/app/api/group/actions";
import { fetchUser } from "@/app/api/user";
import { useSearchParams } from "next/navigation";

interface SubComponents {
Loading: typeof EditUserGroupLoading;


+ 11
- 1
src/components/LeaveModal/LeaveModal.tsx Переглянути файл

@@ -6,6 +6,7 @@ import {
CardActions,
CardContent,
Modal,
ModalProps,
SxProps,
Typography,
} from "@mui/material";
@@ -86,8 +87,17 @@ const LeaveModal: React.FC<Props> = ({
onClose();
}, [defaultValues, formProps, onClose]);

const onModalClose = useCallback<NonNullable<ModalProps["onClose"]>>(
(_, reason) => {
if (reason !== "backdropClick") {
onClose();
}
},
[onClose],
);

return (
<Modal open={isOpen} onClose={onClose}>
<Modal open={isOpen} onClose={onModalClose}>
<Card sx={modalSx}>
<FormProvider {...formProps}>
<CardContent


+ 152
- 0
src/components/ProjectCashFlow/ProjectCashFlow.tsx Переглянути файл

@@ -27,6 +27,9 @@ const ProjectCashFlow: React.FC = () => {
const [cashFlowYear, setCashFlowYear]: any[] = React.useState(
todayDate.getFullYear(),
);
const [anticipateCashFlowYear, setAnticipateCashFlowYear]: any[] = React.useState(
todayDate.getFullYear(),
);
const columns = [
{
id: "projectCode",
@@ -236,6 +239,113 @@ const ProjectCashFlow: React.FC = () => {
],
};

const anticipateOptions: ApexOptions = {
chart: {
height: 350,
type: "line",
},
stroke: {
width: [0, 0, 2, 2],
},
plotOptions: {
bar: {
horizontal: false,
distributed: false,
},
},
dataLabels: {
enabled: false,
},
xaxis: {
categories: [
"Q1",
"Q2",
"Q3",
"Q4",
"Q5",
"Q6",
"Q7",
"Q8",
"Q9",
"Q10",
"Q11",
"Q12",
],
},
yaxis: [
{
title: {
text: "Anticipate Monthly Income and Expenditure(HKD)",
},
min: 0,
max: 350000,
tickAmount: 5,
labels: {
formatter: function (val) {
return val.toLocaleString()
}
}
},
{
show: false,
seriesName: "Monthly_Expenditure",
title: {
text: "Monthly Expenditure (HKD)",
},
min: 0,
max: 350000,
tickAmount: 5,
},
{
seriesName: "Cumulative_Income",
opposite: true,
title: {
text: "Cumulative Income and Expenditure(HKD)",
},
min: 0,
max: 850000,
tickAmount: 5,
labels: {
formatter: function (val) {
return val.toLocaleString()
}
}
},
{
show: false,
seriesName: "Cumulative_Expenditure",
opposite: true,
title: {
text: "Cumulative Expenditure (HKD)",
},
min: 0,
max: 850000,
tickAmount: 5,
},
],
grid: {
borderColor: "#f1f1f1",
},
annotations: {},
series: [
{
name: "Monthly_Income",
type: "column",
color: "#f1c48a",
data: [0, 110000, 0, 0, 185000, 0, 0, 189000, 0, 0, 300000, 0],
},
{
name: "Monthly_Expenditure",
type: "column",
color: "#89d7f3",
data: [
60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000,
60000, 60000,
],
}
],
};

const accountsReceivableOptions: ApexOptions = {
colors: ["#20E647"],
series: [80],
@@ -624,6 +734,48 @@ const ProjectCashFlow: React.FC = () => {
}}
>
<Grid item xs={12} md={12} lg={12}>
<Card>
<CardHeader
className="text-slate-500"
title="Anticipate Cash Flow by Month"
/>
<div style={{ display: "inline-block", width: "99%" }}>
<div className="inline-block">
<Label className="text-slate-500 font-medium ml-6">
Period:&nbsp;
</Label>
<Input
id={"cashFlowYear"}
value={anticipateCashFlowYear}
readOnly={true}
bsSize="lg"
className="rounded-md text-base w-12"
/>
</div>
<div className="inline-block ml-1">
<button
onClick={() => setAnticipateCashFlowYear(cashFlowYear - 1)}
className="hover:cursor-pointer hover:bg-slate-200 bg-transparent rounded-md w-8 h-8 text-base"
>
&lt;
</button>
</div>
<div className="inline-block ml-1">
<button
onClick={() => setAnticipateCashFlowYear(cashFlowYear + 1)}
className="hover:cursor-pointer hover:bg-slate-200 bg-transparent rounded-md w-8 h-8 text-base"
>
&gt;
</button>
</div>
<ReactApexChart
options={anticipateOptions}
series={anticipateOptions.series}
type="line"
height="350"
/>
</div>
</Card>
<Card>
<CardHeader
className="text-slate-500"


+ 3
- 2
src/components/SearchResults/SearchResults.tsx Переглянути файл

@@ -26,6 +26,7 @@ interface BaseColumn<T extends ResultWithId> {
color?: IconButtonOwnProps["color"];
needTranslation?: boolean;
type?: string;
isHidden?: boolean;
}

interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
@@ -78,7 +79,7 @@ function SearchResults<T extends ResultWithId>({
<Table stickyHeader>
<TableHead>
<TableRow>
{columns.map((column, idx) => (
{columns.filter(item => item.isHidden !== true).map((column, idx) => (
<TableCell key={`${column.name.toString()}${idx}`}>
{column?.type === "money" ? <div style={{display: "flex", justifyContent: "flex-end"}}>{column.label}</div> : column.label}
</TableCell>
@@ -91,7 +92,7 @@ function SearchResults<T extends ResultWithId>({
.map((item) => {
return (
<TableRow hover tabIndex={-1} key={item.id}>
{columns.map((column, idx) => {
{columns.filter(item => item.isHidden !== true).map((column, idx) => {
const columnName = column.name;

return (


+ 17
- 15
src/components/SkillSearch/SkillSearch.tsx Переглянути файл

@@ -18,16 +18,21 @@ type SearchParamNames = keyof SearchQuery;

const SkillSearch: React.FC<Props> = ({ skill }) => {
const { t } = useTranslation();
const [filteredStaff, setFilteredStaff] = useState(skill);
const [filteredSkill, setFilteredSkill] = useState(skill);
const router = useRouter();

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{
label: t("Staff Name"),
label: t("Skill Name"),
paramName: "name",
type: "text",
},
{
label: t("Skill code"),
paramName: "code",
type: "text",
},
],
[t]
);
@@ -36,7 +41,7 @@ const SkillSearch: React.FC<Props> = ({ skill }) => {
(skill: SkillResult) => {
console.log(skill);
const id = skill.id;
// router.push(`/settings/skill/edit?id=${id}`);
router.push(`/settings/skill/edit?id=${id}`);
},
[router, t]
);
@@ -45,7 +50,7 @@ const SkillSearch: React.FC<Props> = ({ skill }) => {
// deleteDialog(async () => {
// await deleteStaff(skill.id);
// successDialog("Delete Success", t);
// setFilteredStaff((prev) => prev.filter((obj) => obj.id !== skill.id));
// setFilteredSkill((prev) => prev.filter((obj) => obj.id !== skill.id));
// }, t);
}, []);

@@ -76,19 +81,16 @@ const SkillSearch: React.FC<Props> = ({ skill }) => {
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
// setFilteredStaff(
// skill.filter(
// (s) =>
// s.skillId.toLowerCase().includes(query.skillId.toLowerCase()) &&
// s.name.toLowerCase().includes(query.name.toLowerCase())
// // (query.team === "All" || s.team === query.team) &&
// // (query.category === "All" || s.category === query.category) &&
// // (query.team === "All" || s.team === query.team),
// )
// );
setFilteredSkill(
skill.filter(
(s) =>
s.name.toLowerCase().includes(query.name.toLowerCase()) &&
s.code.toLowerCase().includes(query.code.toLowerCase())
)
);
}}
/>
<SearchResults<SkillResult> items={filteredStaff} columns={columns} />
<SearchResults<SkillResult> items={filteredSkill} columns={columns} />
</>
);
};


+ 4
- 1
src/components/StaffSearch/StaffSearch.tsx Переглянути файл

@@ -10,15 +10,17 @@ import { deleteStaff } from "@/app/api/staff/actions";
import { useRouter } from "next/navigation";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import Person from '@mui/icons-material/Person';
import { MAINTAIN_USER, VIEW_USER } from "@/middleware";

interface Props {
staff: StaffResult[];
abilities: string[]
}

type SearchQuery = Partial<Omit<StaffResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const StaffSearch: React.FC<Props> = ({ staff }) => {
const StaffSearch: React.FC<Props> = ({ staff, abilities }) => {
const { t } = useTranslation();
const [filteredStaff, setFilteredStaff] = useState(staff);
const router = useRouter();
@@ -95,6 +97,7 @@ const StaffSearch: React.FC<Props> = ({ staff }) => {
label: t("Actions"),
onClick: onUserClick,
buttonIcon: <Person />,
isHidden: ![MAINTAIN_USER, VIEW_USER].some((ability) => abilities.includes(ability)),
},
{ name: "team", label: t("Team") },
{ name: "name", label: t("Staff Name") },


+ 2
- 10
src/components/StaffSearch/StaffSearchWrapper.tsx Переглянути файл

@@ -2,16 +2,8 @@ import { fetchStaff, fetchTeamLeads } from "@/app/api/staff";
import React from "react";
import StaffSearch from "./StaffSearch";
import StaffSearchLoading from "./StaffSearchLoading";
import { comboProp, fetchCompanyCombo } from "@/app/api/companys/actions";
import { fetchTeamCombo } from "@/app/api/team/actions";
import { fetchDepartmentCombo } from "@/app/api/departments/actions";
import { fetchPositionCombo } from "@/app/api/positions/actions";
import { fetchGradeCombo } from "@/app/api/grades/actions";
import { fetchSkillCombo } from "@/app/api/skill/actions";
import { fetchSalaryCombo } from "@/app/api/salarys/actions";
import { Session, getServerSession } from "next-auth";
import { authOptions } from "@/config/authConfig";
// import { preloadStaff } from "@/app/api/staff";

interface SubComponents {
Loading: typeof StaffSearchLoading;
@@ -24,9 +16,9 @@ interface SessionWithAbilities extends Session {
const StaffSearchWrapper: React.FC & SubComponents = async () => {
const staff = await fetchStaff();
const session = await getServerSession(authOptions) as SessionWithAbilities;
console.log(session.abilities);
const abilities: string[] = session.abilities!!

return <StaffSearch staff={staff} />;
return <StaffSearch staff={staff} abilities={abilities}/>;
};

StaffSearchWrapper.Loading = StaffSearchLoading;


+ 240
- 1
src/components/StaffUtilization/StaffUtilization.tsx Переглянути файл

@@ -124,6 +124,8 @@ const StaffUtilization: React.FC = () => {
const [manHoursSpentPeriod, setManHoursSpentPeriod]: any[] = React.useState(
firstDayOfWeekString + " to " + lastDayOfWeekString,
);
const [unsubmittedTimeSheetSelect, setUnsubmittedTimeSheetSelect]: any =
React.useState("Weekly");
const [teamTotalManhoursSpentSelect, setTeamTotalManhoursSpentSelect]: any =
React.useState("Weekly");
const [staffGradeManhoursSpentSelect, setStaffGradeManhoursSpentSelect]: any =
@@ -165,6 +167,10 @@ const StaffUtilization: React.FC = () => {
individualStaffManhoursSpentPeriod,
setIndividualStaffManhoursSpentPeriod,
]: any[] = React.useState(weekDates);
const [
unsubmittedTimeSheetPeriod,
setUnsubmittedTimeSheetPeriod,
]: any[] = React.useState(weekDates);
const [
teamTotalManhoursSpentPlanData,
setTeamTotalManhoursSpentPlanData,
@@ -179,6 +185,8 @@ const StaffUtilization: React.FC = () => {
React.useState<Dayjs | null>(dayjs());
const [weeklyValueByIndividualStaff, setWeeklyValueByIndividualStaff] =
React.useState<Dayjs | null>(dayjs());
const [weeklyUnsubmittedTimeSheet, setWeeklyUnsubmittedTimeSheet ] =
React.useState<Dayjs | null>(dayjs());
const [staffGradeManhoursSpentValue, setStaffGradeManhoursSpentValue] =
React.useState<Dayjs | null>(dayjs());
const [totalManHoursMonthlyFromValue, setTotalManHoursMonthlyFromValue] =
@@ -424,6 +432,56 @@ const StaffUtilization: React.FC = () => {
],
};

const unsubmittedTimeSheetOptions: ApexOptions = {
chart: {
height: 350,
type: "line",
},
stroke: {
width: [1],
},
plotOptions: {
bar: {
horizontal: true,
distributed: false,
},
},
dataLabels: {
enabled: true,
},
xaxis: {
categories: [
"001-Staff A",
"002-Staff B",
"005-Staff E",
"006-Staff F",
"007-Staff G",
],
},
yaxis: [
{
title: {
text: "Staff",
},
min: 0,
max: 12,
tickAmount: 5,
},
],
grid: {
borderColor: "#f1f1f1",
},
annotations: {},
series: [
{
name: "Unsubmitted Time Sheet",
type: "bar",
color: "#00acb1",
data: [2, 2, 1, 5, 1],
},
],
};

const teamTotalManhoursSpentOnClick = (r: any) => {
setTeamTotalManhoursSpentSelect(r);
if (r === "Weekly") {
@@ -456,6 +514,10 @@ const StaffUtilization: React.FC = () => {
// }
};

const unsubmittedTimeSheetOnClick = (r: any) => {
setUnsubmittedTimeSheetSelect(r);
};

const selectWeeklyPeriod = (r: any) => {
const selectDate = new Date(r);
const firstDayOfWeek = new Date();
@@ -486,6 +548,21 @@ const StaffUtilization: React.FC = () => {
setWeeklyValueByStaffGrade(dayjs(firstDayOfWeek));
};

const selectWeeklyPeriodUnsubmittedTimeSheet = (r: any) => {
const selectDate = new Date(r);
const firstDayOfWeek = new Date();
firstDayOfWeek.setDate(selectDate.getDate() - selectDate.getDay() + 0);
const weekDates: any[] = [];
for (let i = 0; i < 7; i++) {
const currentDate = new Date(firstDayOfWeek);
currentDate.setDate(firstDayOfWeek.getDate() + i);
const formattedDate = dayjs(currentDate).format("DD MMM (ddd)");
weekDates.push(formattedDate);
}
setUnsubmittedTimeSheetPeriod(weekDates);
setWeeklyUnsubmittedTimeSheet(dayjs(firstDayOfWeek));
};

const selectWeeklyPeriodIndividualStaff = (r: any) => {
const selectDate = new Date(r);
const firstDayOfWeek = new Date();
@@ -589,6 +666,28 @@ const StaffUtilization: React.FC = () => {
setTeamTotalManhoursByStaffGrade(weekDates);
};

const selectUnsubmittedTimeSheetMonthlyPeriodFrom = (r: any) => {
const monthDates: any[] = [];
const monthPlanData: any[] = [];
const monthActualData: any[] = [];
const selectFromDate = dayjs(r);
for (
let date = selectFromDate.clone();
date.isBefore(totalManHoursMonthlyToValue, "month");
date = date.add(1, "month")
) {
monthDates.push(date.format("MM-YYYY"));
monthPlanData.push(840);
monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840));
}
monthDates.push(totalManHoursMonthlyToValue.format("MM-YYYY"));
monthPlanData.push(840);
monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840));
// setTeamTotalManhoursSpentPlanData(monthPlanData)
// setTeamTotalManhoursSpentActualData(monthActualData)
setUnsubmittedTimeSheetPeriod(weekDates);
};

const selectIndividualStaffMonthlyPeriodFrom = (r: any) => {
const monthDates: any[] = [];
const monthPlanData: any[] = [];
@@ -611,6 +710,28 @@ const StaffUtilization: React.FC = () => {
setIndividualStaffManhoursSpentPeriod(weekDates);
};

const selectUnsubmittedTimeSheetMonthlyPeriodTo = (r: any) => {
const monthDates: any[] = [];
const monthPlanData: any[] = [];
const monthActualData: any[] = [];
const selectToDate = dayjs(r);
for (
let date = totalManHoursMonthlyFromValue.clone();
date.isBefore(selectToDate, "month");
date = date.add(1, "month")
) {
monthDates.push(date.format("MM-YYYY"));
monthPlanData.push(840);
monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840));
}
monthDates.push(selectToDate.format("MM-YYYY"));
monthPlanData.push(840);
monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840));
// setTeamTotalManhoursSpentPlanData(monthPlanData)
// setTeamTotalManhoursSpentActualData(monthActualData)
setUnsubmittedTimeSheetPeriod(weekDates);
};

const selectIndividualStaffMonthlyPeriodTo = (r: any) => {
const monthDates: any[] = [];
const monthPlanData: any[] = [];
@@ -934,7 +1055,125 @@ const StaffUtilization: React.FC = () => {
}}
>
<Grid item xs={12} md={12} lg={12}>
<Card>
<Card className="mb-5">
<CardHeader
className="text-slate-500"
title="Unsubmitted Time Sheet by Staff"
/>
<div style={{ display: "inline-block", width: "99%" }}>
<div className="w-fit align-top mr-5 float-right">
{unsubmittedTimeSheetSelect === "Weekly" && (
<>
<button className="text-lg bg-violet-100 border-violet-500 text-violet-500 border-solid w-32">
Weekly
</button>
<button
onClick={() =>
unsubmittedTimeSheetOnClick("Monthly")
}
className="hover:cursor-pointer hover:bg-violet-50 text-lg bg-transparent border-violet-500 text-violet-500 border-solid rounded-r-md w-32"
>
Monthly
</button>
</>
)}
{unsubmittedTimeSheetSelect === "Monthly" && (
<>
<button
onClick={() =>
unsubmittedTimeSheetOnClick("Weekly")
}
className="hover:cursor-pointer hover:bg-violet-50 text-lg bg-transparent border-violet-500 text-violet-500 border-solid w-32"
>
Weekly
</button>
<button className="text-lg bg-violet-100 border-violet-500 text-violet-500 border-solid rounded-r-md w-32">
Monthly
</button>
</>
)}
</div>
<div className="inline-block w-fit mt-2">
<div className="inline-block ml-6">
<Label className="text-slate-500 font-medium">
Team:&nbsp;
</Label>
</div>
<div className="inline-block ml-1 w-60">
<Select
placeholder="Team"
options={teamOptions}
isClearable={true}
/>
</div>
<div className="ml-6 mt-2" style={{ verticalAlign: "top" }}>
{/* <Label className="text-slate-500 font-medium ml-6">
Period:&nbsp;
</Label> */}
{unsubmittedTimeSheetSelect === "Weekly" && (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
className="w-72 h-10 align-top"
label="Period:"
value={value}
format="DD-MM-YYYY"
onChange={(newValue) =>
selectWeeklyPeriodUnsubmittedTimeSheet(newValue)
}
showDaysOutsideCurrentMonth
displayWeekNumber
slots={{ day: Day }}
slotProps={{
day: (ownerState) =>
({
selectedDay: value,
hoveredDay,
onPointerEnter: () =>
setHoveredDay(ownerState.day),
onPointerLeave: () => setHoveredDay(null),
}) as any,
}}
/>
</LocalizationProvider>
)}
{unsubmittedTimeSheetSelect === "Monthly" && (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
className="w-40 h-10 align-top"
onChange={(newValue) =>
selectUnsubmittedTimeSheetMonthlyPeriodFrom(newValue)
}
defaultValue={
totalManHoursByIndividualStaffMonthlyFromValue
}
label={"Form"}
views={["month", "year"]}
/>
<DatePicker
className="w-40 h-10 align-top"
onChange={(newValue) =>
selectUnsubmittedTimeSheetMonthlyPeriodTo(newValue)
}
defaultValue={
totalManHoursByIndividualStaffMonthlyToValue
}
label={"To"}
views={["month", "year"]}
/>
</LocalizationProvider>
)}
</div>
</div>

<ReactApexChart
options={unsubmittedTimeSheetOptions}
series={unsubmittedTimeSheetOptions.series}
type="bar"
height="380"
/>
</div>
</Card>
<Card>
<Card>
<CardHeader
className="text-slate-500"


+ 7
- 3
src/components/Swal/CustomAlerts.js Переглянути файл

@@ -61,13 +61,17 @@ export const warningDialog = (text, t) => {
})
}

export const submitDialog = async (confirmAction, t, {...props}) => {
export const submitDialog = async (confirmAction, t, {...props} = {
title: t("Do you want to submit?"),
confirmButtonText: t("Submit"),
}) => {
console.log(props)
// const { t } = useTranslation("common")
const result = await Swal.fire({
icon: "question",
title: props.title ?? t("Do you want to submit?"),
title: props?.title,
cancelButtonText: t("Cancel"),
confirmButtonText: props.confirmButtonText ?? t("Submit"),
confirmButtonText: props?.confirmButtonText,
showCancelButton: true,
showConfirmButton: true,
customClass: {


+ 11
- 1
src/components/TimesheetModal/TimesheetModal.tsx Переглянути файл

@@ -6,6 +6,7 @@ import {
CardActions,
CardContent,
Modal,
ModalProps,
SxProps,
Typography,
} from "@mui/material";
@@ -91,8 +92,17 @@ const TimesheetModal: React.FC<Props> = ({
onClose();
}, [defaultValues, formProps, onClose]);

const onModalClose = useCallback<NonNullable<ModalProps["onClose"]>>(
(_, reason) => {
if (reason !== "backdropClick") {
onClose();
}
},
[onClose],
);

return (
<Modal open={isOpen} onClose={onClose}>
<Modal open={isOpen} onClose={onModalClose}>
<Card sx={modalSx}>
<FormProvider {...formProps}>
<CardContent


+ 13
- 4
src/components/TimesheetTable/EntryInputTable.tsx Переглянути файл

@@ -211,7 +211,7 @@ const EntryInputTable: React.FC<Props> = ({
{
field: "projectId",
headerName: t("Project Code and Name"),
width: 400,
width: 300,
editable: true,
valueFormatter(params) {
const project = assignedProjects.find((p) => p.id === params.value);
@@ -310,7 +310,17 @@ const EntryInputTable: React.FC<Props> = ({
editable: true,
type: "number",
valueFormatter(params) {
return manhourFormatter.format(params.value);
return manhourFormatter.format(params.value || 0);
},
},
{
field: "otHours",
headerName: t("Other Hours"),
width: 150,
editable: true,
type: "number",
valueFormatter(params) {
return manhourFormatter.format(params.value || 0);
},
},
{
@@ -336,10 +346,9 @@ const EntryInputTable: React.FC<Props> = ({
useEffect(() => {
setValue(day, [
...entries
.filter((e) => !e._isNew && !e._error && e.id && e.inputHours)
.filter((e) => !e._isNew && !e._error && e.id)
.map(({ isPlanned, _error, _isNew, ...entry }) => ({
id: entry.id!,
inputHours: entry.inputHours!,
...entry,
})),
]);


+ 4
- 1
src/components/TimesheetTable/TimesheetTable.tsx Переглянути файл

@@ -75,7 +75,10 @@ const DayRow: React.FC<{
const dayJsObj = dayjs(day);
const [open, setOpen] = useState(false);

const totalHours = entries.reduce((acc, entry) => acc + entry.inputHours, 0);
const totalHours = entries.reduce(
(acc, entry) => acc + (entry.inputHours || 0) + (entry.otHours || 0),
0,
);

return (
<>


Завантаження…
Відмінити
Зберегти