Bladeren bron

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

# Conflicts:
#	src/components/NavigationContent/NavigationContent.tsx
tags/Baseline_30082024_FRONTEND_UAT
MSI\2Fi 1 jaar geleden
bovenliggende
commit
082b148689
100 gewijzigde bestanden met toevoegingen van 3060 en 352 verwijderingen
  1. +230
    -3
      package-lock.json
  2. BIN
      public/temp/AR04_Cost and Expense Report.xlsx
  3. BIN
      public/temp/AR05_Project Completion Report.xlsx
  4. BIN
      public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx
  5. BIN
      public/temp/AR07_Project Claims Report.xlsx
  6. BIN
      public/temp/AR08_Project P&L Report.xlsx
  7. BIN
      public/temp/EX01_Financial Status Report.xlsx
  8. +25
    -0
      src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx
  9. +24
    -0
      src/app/(main)/analytics/FinancialStatusReport/page.tsx
  10. +24
    -0
      src/app/(main)/analytics/ProjectClaimsReport/page.tsx
  11. +29
    -0
      src/app/(main)/dashboard/ProjectResourceSummary/page.tsx
  12. +1
    -1
      src/app/(main)/dashboard/StaffUtilization/page.tsx
  13. +17
    -0
      src/app/(main)/projects/edit/not-found.tsx
  14. +74
    -0
      src/app/(main)/projects/edit/page.tsx
  15. +2
    -2
      src/app/(main)/settings/customer/create/page.tsx
  16. +2
    -2
      src/app/(main)/settings/customer/edit/page.tsx
  17. +48
    -0
      src/app/(main)/settings/skill/create/page.tsx
  18. +50
    -0
      src/app/(main)/settings/skill/page.tsx
  19. +54
    -0
      src/app/(main)/settings/user/page.tsx
  20. +2
    -2
      src/app/(main)/staffReimbursement/create/page.tsx
  21. +4
    -1
      src/app/(main)/tasks/create/page.tsx
  22. +26
    -0
      src/app/(main)/tasks/edit/page.tsx
  23. +8
    -5
      src/app/(main)/tasks/page.tsx
  24. +1
    -1
      src/app/api/claims/actions.ts
  25. +3
    -3
      src/app/api/clientprojects/index.ts
  26. +31
    -3
      src/app/api/projects/actions.ts
  27. +26
    -2
      src/app/api/projects/index.ts
  28. +42
    -0
      src/app/api/report7/index.ts
  29. +42
    -0
      src/app/api/reporte1/index.ts
  30. +23
    -0
      src/app/api/reports/actions.ts
  31. +8
    -0
      src/app/api/reports/index.ts
  32. +53
    -0
      src/app/api/resourcesummary/index.ts
  33. +17
    -1
      src/app/api/skill/actions.ts
  34. +22
    -0
      src/app/api/skill/index.ts
  35. +3
    -3
      src/app/api/staff/actions.ts
  36. +28
    -2
      src/app/api/tasks/actions.ts
  37. +3
    -4
      src/app/api/team/actions.ts
  38. +1
    -0
      src/app/api/team/index.ts
  39. +27
    -0
      src/app/api/user/actions.ts
  40. +43
    -0
      src/app/api/user/index.ts
  41. +8
    -0
      src/app/utils/commonUtil.ts
  42. +91
    -2
      src/app/utils/fetchUtil.ts
  43. +2
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  44. +0
    -1
      src/components/ClaimDetail/index.ts
  45. +0
    -0
      src/components/ClaimSave/ClaimFormInfo.tsx
  46. +5
    -4
      src/components/ClaimSave/ClaimFormInputGrid.tsx
  47. +11
    -10
      src/components/ClaimSave/ClaimSave.tsx
  48. +2
    -2
      src/components/ClaimSave/ClaimSaveWrapper.tsx
  49. +1
    -0
      src/components/ClaimSave/index.ts
  50. +6
    -6
      src/components/ClaimSearch/ClaimSearch.tsx
  51. +163
    -78
      src/components/CreateProject/CreateProject.tsx
  52. +16
    -1
      src/components/CreateProject/CreateProjectWrapper.tsx
  53. +122
    -0
      src/components/CreateSkill/CreateSkill.tsx
  54. +40
    -0
      src/components/CreateSkill/CreateSkillLoading.tsx
  55. +19
    -0
      src/components/CreateSkill/CreateSkillWrapper.tsx
  56. +90
    -0
      src/components/CreateSkill/SkillInfo.tsx
  57. +1
    -0
      src/components/CreateSkill/index.ts
  58. +1
    -1
      src/components/CreateStaff/CreateStaff.tsx
  59. +122
    -69
      src/components/CreateTaskTemplate/CreateTaskTemplate.tsx
  60. +1
    -1
      src/components/CreateTeam/CreateTeam.tsx
  61. +94
    -80
      src/components/CreateTeam/StaffAllocation.tsx
  62. +15
    -3
      src/components/CustomDatagrid/CustomDatagrid.tsx
  63. +65
    -3
      src/components/CustomInputForm/CustomInputForm.tsx
  64. +0
    -1
      src/components/CustomerDetail/index.ts
  65. +0
    -0
      src/components/CustomerSave/ContactInfo.tsx
  66. +0
    -0
      src/components/CustomerSave/CustomerInfo.tsx
  67. +16
    -16
      src/components/CustomerSave/CustomerSave.tsx
  68. +4
    -4
      src/components/CustomerSave/CustomerSaveWrapper.tsx
  69. +0
    -0
      src/components/CustomerSave/SubsidiaryAllocation.tsx
  70. +1
    -0
      src/components/CustomerSave/index.ts
  71. +22
    -7
      src/components/EditStaff/EditStaff.tsx
  72. +13
    -5
      src/components/EditTeam/Allocation.tsx
  73. +33
    -6
      src/components/EditTeam/EditTeam.tsx
  74. +21
    -9
      src/components/EditTeam/EditTeamWrapper.tsx
  75. +49
    -0
      src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx
  76. +38
    -0
      src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx
  77. +18
    -0
      src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx
  78. +1
    -0
      src/components/GenerateEX02ProjectCashFlowReport/index.ts
  79. +14
    -0
      src/components/NavigationContent/NavigationContent.tsx
  80. +2
    -1
      src/components/ProgressByClient/ProgressByClient.tsx
  81. +18
    -3
      src/components/ProgressByClientSearch/ProgressByClientSearch.tsx
  82. +2
    -1
      src/components/ProgressByTeam/ProgressByTeam.tsx
  83. +548
    -0
      src/components/ProjectResourceSummary/ProjectResourceSummary.tsx
  84. +1
    -0
      src/components/ProjectResourceSummary/index.ts
  85. +75
    -0
      src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx
  86. +40
    -0
      src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx
  87. +20
    -0
      src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx
  88. +1
    -0
      src/components/ProjectResourceSummarySearch/index.ts
  89. +8
    -3
      src/components/ProjectSearch/ProjectSearch.tsx
  90. +17
    -0
      src/components/Report/FinancialStatusReport/FinancialStatusReport.tsx
  91. +2
    -0
      src/components/Report/FinancialStatusReport/index.ts
  92. +43
    -0
      src/components/Report/FinancialStatusReportGen/FinancialStatusReportGen.tsx
  93. +41
    -0
      src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenLoading.tsx
  94. +19
    -0
      src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenWrapper.tsx
  95. +2
    -0
      src/components/Report/FinancialStatusReportGen/index.ts
  96. +17
    -0
      src/components/Report/ProjectClaimsReport/ProjectClaimsReport.tsx
  97. +2
    -0
      src/components/Report/ProjectClaimsReport/index.ts
  98. +44
    -0
      src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGen.tsx
  99. +41
    -0
      src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenLoading.tsx
  100. +19
    -0
      src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenWrapper.tsx

+ 230
- 3
package-lock.json Bestand weergeven

@@ -11,6 +11,7 @@
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@faker-js/faker": "^8.4.1",
"@fontsource/inter": "^5.0.16",
"@fontsource/plus-jakarta-sans": "^5.0.18",
"@mui/icons-material": "^5.15.0",
@@ -20,6 +21,7 @@
"@mui/x-date-pickers": "^6.18.7",
"@unly/universal-language-detector": "^2.0.3",
"apexcharts": "^3.45.2",
"axios": "^1.6.8",
"dayjs": "^1.11.10",
"i18next": "^23.7.11",
"i18next-resources-to-backend": "^1.2.0",
@@ -37,7 +39,8 @@
"react-select": "^5.8.0",
"reactstrap": "^9.2.2",
"styled-components": "^6.1.8",
"sweetalert2": "^11.10.3"
"sweetalert2": "^11.10.3",
"xlsx-js-style": "^1.2.0"
},
"devDependencies": {
"@types/lodash": "^4.14.202",
@@ -1933,6 +1936,21 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@faker-js/faker": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz",
"integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
"npm": ">=6.14.13"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
@@ -3530,6 +3548,21 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/adler-32": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz",
"integrity": "sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==",
"dependencies": {
"exit-on-epipe": "~1.0.1",
"printj": "~1.1.0"
},
"bin": {
"adler32": "bin/adler32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -3836,6 +3869,11 @@
"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",
@@ -3904,6 +3942,16 @@
"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",
@@ -4150,6 +4198,26 @@
}
]
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cfb/node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -4236,6 +4304,26 @@
"node": ">=6"
}
},
"node_modules/codepage": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz",
"integrity": "sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==",
"dependencies": {
"commander": "~2.14.1",
"exit-on-epipe": "~1.0.1"
},
"bin": {
"codepage": "bin/codepage.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/codepage/node_modules/commander": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw=="
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -4249,6 +4337,17 @@
"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",
@@ -4316,6 +4415,17 @@
"node": ">=10"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -4516,6 +4626,14 @@
"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",
@@ -5431,6 +5549,14 @@
"node": ">=0.8.x"
}
},
"node_modules/exit-on-epipe": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
"integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5476,6 +5602,11 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz",
"integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A=="
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -5575,6 +5706,25 @@
"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",
@@ -5599,6 +5749,14 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -6977,7 +7135,6 @@
"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"
}
@@ -6986,7 +7143,6 @@
"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"
},
@@ -7880,6 +8036,17 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
},
"node_modules/printj": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==",
"bin": {
"printj": "bin/printj.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -7890,6 +8057,11 @@
"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",
@@ -8631,6 +8803,17 @@
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"deprecated": "Please use @jridgewell/sourcemap-codec instead"
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -9853,6 +10036,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/workbox-background-sync": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz",
@@ -10238,6 +10437,34 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/xlsx-js-style": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz",
"integrity": "sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==",
"dependencies": {
"adler-32": "~1.2.0",
"cfb": "^1.1.4",
"codepage": "~1.14.0",
"commander": "~2.17.1",
"crc-32": "~1.2.0",
"exit-on-epipe": "~1.0.1",
"fflate": "^0.3.8",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xlsx-js-style/node_modules/commander": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg=="
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",


BIN
public/temp/AR04_Cost and Expense Report.xlsx Bestand weergeven


BIN
public/temp/AR05_Project Completion Report.xlsx Bestand weergeven


BIN
public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx Bestand weergeven


BIN
public/temp/AR07_Project Claims Report.xlsx Bestand weergeven


BIN
public/temp/AR08_Project P&L Report.xlsx Bestand weergeven


BIN
public/temp/EX01_Financial Status Report.xlsx Bestand weergeven


+ 25
- 0
src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx Bestand weergeven

@@ -0,0 +1,25 @@
import { Metadata } from "next";
import { Suspense } from "react";
import { I18nProvider } from "@/i18n";
import { fetchProjects } from "@/app/api/projects";
import GenerateEX02ProjectCashFlowReport from "@/components/GenerateEX02ProjectCashFlowReport";

export const metadata: Metadata = {
title: "EX02 - Project Cash Flow Report",
};

const ProjectCashFlowReport: React.FC = async () => {
fetchProjects();

return (
<>
<I18nProvider namespaces={["report", "common"]}>
<Suspense fallback={<GenerateEX02ProjectCashFlowReport.Loading />}>
<GenerateEX02ProjectCashFlowReport />
</Suspense>
</I18nProvider>
</>
);
};

export default ProjectCashFlowReport;

+ 24
- 0
src/app/(main)/analytics/FinancialStatusReport/page.tsx Bestand weergeven

@@ -0,0 +1,24 @@
//src\app\(main)\analytics\DelayReport\page.tsx
import { Metadata } from "next";
import { I18nProvider } from "@/i18n";
import Typography from "@mui/material/Typography";
import FinancialStatusReportComponent from "@/components/Report/FinancialStatusReport";

export const metadata: Metadata = {
title: "Financial Status Report",
};

const ProjectFinancialStatusReport: React.FC = () => {
return (
<I18nProvider namespaces={["analytics"]}>
<Typography variant="h4" marginInlineEnd={2}>
Financial Status Report
</Typography>
{/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}>
<ProgressCashFlowSearch/>
</Suspense> */}
<FinancialStatusReportComponent />
</I18nProvider>
);
};
export default ProjectFinancialStatusReport;

+ 24
- 0
src/app/(main)/analytics/ProjectClaimsReport/page.tsx Bestand weergeven

@@ -0,0 +1,24 @@
//src\app\(main)\analytics\DelayReport\page.tsx
import { Metadata } from "next";
import { I18nProvider } from "@/i18n";
import Typography from "@mui/material/Typography";
import ProjectClaimsReportComponent from "@/components/Report/ProjectClaimsReport";

export const metadata: Metadata = {
title: "Project Claims Report",
};

const ProjectClaimsReport: React.FC = () => {
return (
<I18nProvider namespaces={["analytics"]}>
<Typography variant="h4" marginInlineEnd={2}>
Project Claims Report
</Typography>
{/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}>
<ProgressCashFlowSearch/>
</Suspense> */}
<ProjectClaimsReportComponent />
</I18nProvider>
);
};
export default ProjectClaimsReport;

+ 29
- 0
src/app/(main)/dashboard/ProjectResourceSummary/page.tsx Bestand weergeven

@@ -0,0 +1,29 @@
import { Metadata } from "next";
import { I18nProvider } from "@/i18n";
import DashboardPage from "@/components/DashboardPage/DashboardPage";
import DashboardPageButton from "@/components/DashboardPage/DashboardTabButton";
import { Suspense } from "react";
import Tabs, { TabsProps } from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Typography from "@mui/material/Typography";
import StaffUtilizationComponent from "@/components/StaffUtilization";
import ProjectResourceSummarySearch from "@/components/ProjectResourceSummarySearch";
import { ResourceSummaryResult } from "@/app/api/resourcesummary";

export const metadata: Metadata = {
title: "Project Resource Summary",
};

const ProjectResourceSummary: React.FC = () => {
return (
<I18nProvider namespaces={["dashboard"]}>
<Typography variant="h4" marginInlineEnd={2}>
Project Resource Summary
</Typography>
<Suspense fallback={<ProjectResourceSummarySearch.Loading />}>
<ProjectResourceSummarySearch/>
</Suspense>
</I18nProvider>
);
};
export default ProjectResourceSummary;

+ 1
- 1
src/app/(main)/dashboard/StaffUtilization/page.tsx Bestand weergeven

@@ -10,7 +10,7 @@ import Typography from "@mui/material/Typography";
import StaffUtilizationComponent from "@/components/StaffUtilization";

export const metadata: Metadata = {
title: "Project Status by Client",
title: "Staff Utilization",
};

const StaffUtilization: React.FC = () => {


+ 17
- 0
src/app/(main)/projects/edit/not-found.tsx Bestand weergeven

@@ -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>
);
}

+ 74
- 0
src/app/(main)/projects/edit/page.tsx Bestand weergeven

@@ -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;

+ 2
- 2
src/app/(main)/settings/customer/create/page.tsx Bestand weergeven

@@ -1,4 +1,4 @@
import CustomerDetail from "@/components/CustomerDetail";
import CustomerSave from "@/components/CustomerSave";
// import { preloadAllTasks } from "@/app/api/tasks";
import CreateTaskTemplate from "@/components/CreateTaskTemplate";
import { I18nProvider, getServerI18n } from "@/i18n";
@@ -16,7 +16,7 @@ const CreateCustomer: React.FC = async () => {
<>
<Typography variant="h4">{t("Create Customer")}</Typography>
<I18nProvider namespaces={["customer", "common"]}>
<CustomerDetail />
<CustomerSave />
</I18nProvider>
</>
);


+ 2
- 2
src/app/(main)/settings/customer/edit/page.tsx Bestand weergeven

@@ -1,5 +1,5 @@
import { fetchAllSubsidiaries, preloadAllCustomers } from "@/app/api/customer";
import CustomerDetail from "@/components/CustomerDetail";
import CustomerSave from "@/components/CustomerSave";
// import { preloadAllTasks } from "@/app/api/tasks";
import CreateTaskTemplate from "@/components/CreateTaskTemplate";
import { I18nProvider, getServerI18n } from "@/i18n";
@@ -18,7 +18,7 @@ const EditCustomer: React.FC = async () => {
<>
<Typography variant="h4">{t("Edit Customer")}</Typography>
<I18nProvider namespaces={["customer", "common"]}>
<CustomerDetail />
<CustomerSave />
</I18nProvider>
</>
);


+ 48
- 0
src/app/(main)/settings/skill/create/page.tsx Bestand weergeven

@@ -0,0 +1,48 @@
// 'use client';
import { I18nProvider, getServerI18n } from "@/i18n";
import CustomInputForm from "@/components/CustomInputForm";
import Check from "@mui/icons-material/Check";
import Close from "@mui/icons-material/Close";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Tab from "@mui/material/Tab";
import Tabs, { TabsProps } from "@mui/material/Tabs";
import { useRouter } from "next/navigation";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Task, TaskTemplate } from "@/app/api/tasks";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
} from "react-hook-form";
import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions";
import { Error } from "@mui/icons-material";
import { ProjectCategory } from "@/app/api/projects";
import { Grid, Typography } from "@mui/material";
import CreateStaffForm from "@/components/CreateStaff/CreateStaff";
import CreateSkill from "@/components/CreateSkill";

// const Title = ["title1", "title2"];

const CreateStaff: React.FC = async () => {
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 (
<>
<Typography variant="h4">{t("Create Skill")}</Typography>
<I18nProvider namespaces={["skill"]}>
<CreateSkill
/>
</I18nProvider>
</>
);
};

export default CreateStaff;

+ 50
- 0
src/app/(main)/settings/skill/page.tsx Bestand weergeven

@@ -0,0 +1,50 @@
import { preloadClaims } from "@/app/api/claims";
// import { preloadSkill, preloadTeamLeads } from "@/app/api/staff";
import SkillSearch from "@/components/SkillSearch";
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: "Skill",
};

const Skill: React.FC = async () => {
const { t } = await getServerI18n("skill");
// preloadTeamLeads();
// preloadSkill();
return (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Skill")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/settings/skill/create"
>
{t("Create Skill")}
</Button>
</Stack>
<I18nProvider namespaces={["staff", "common"]}>
<Suspense fallback={<SkillSearch.Loading />}>
<SkillSearch />
</Suspense>
</I18nProvider>
</>
);
};

export default Skill;

+ 54
- 0
src/app/(main)/settings/user/page.tsx Bestand weergeven

@@ -0,0 +1,54 @@
import { preloadClaims } from "@/app/api/claims";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import StaffSearch from "@/components/StaffSearch";
import TeamSearch from "@/components/TeamSearch";
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",
};


const User: React.FC = async () => {
const { t } = await getServerI18n("User");
// preloadTeamLeads();
// preloadStaff();
return (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("User")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/settings/team/create"
>
{t("Create User")}
</Button>
</Stack>
<I18nProvider namespaces={["User", "common"]}>
<Suspense fallback={<UserSearch.Loading />}>
<UserSearch />
</Suspense>
</I18nProvider>
</>
);
};
export default User;

+ 2
- 2
src/app/(main)/staffReimbursement/create/page.tsx Bestand weergeven

@@ -1,4 +1,4 @@
import ClaimDetail from "@/components/ClaimDetail";
import ClaimSave from "@/components/ClaimSave";
import { I18nProvider, getServerI18n } from "@/i18n";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
@@ -14,7 +14,7 @@ const ClaimDetails: React.FC = async () => {
<>
<Typography variant="h4">{t("Create Claim")}</Typography>
<I18nProvider namespaces={["claim", "common"]}>
<ClaimDetail />
<ClaimSave />
</I18nProvider>
</>
);


+ 4
- 1
src/app/(main)/tasks/create/page.tsx Bestand weergeven

@@ -3,6 +3,7 @@ import CreateTaskTemplate from "@/components/CreateTaskTemplate";
import { getServerI18n } from "@/i18n";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import { I18nProvider } from "@/i18n";

export const metadata: Metadata = {
title: "Create Task Template",
@@ -15,7 +16,9 @@ const Projects: React.FC = async () => {
return (
<>
<Typography variant="h4">{t("Create Task Template")}</Typography>
<CreateTaskTemplate />
<I18nProvider namespaces={["tasks", "common"]}>
<CreateTaskTemplate />
</I18nProvider>
</>
);
};


+ 26
- 0
src/app/(main)/tasks/edit/page.tsx Bestand weergeven

@@ -0,0 +1,26 @@
import { 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";

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

const TaskTemplates: React.FC = async () => {
const { t } = await getServerI18n("tasks");
preloadAllTasks();

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

export default TaskTemplates;

+ 8
- 5
src/app/(main)/tasks/page.tsx Bestand weergeven

@@ -8,13 +8,14 @@ import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";
import { I18nProvider } from "@/i18n";

export const metadata: Metadata = {
title: "Tasks",
};

const TaskTemplates: React.FC = async () => {
const { t } = await getServerI18n("projects");
const { t } = await getServerI18n("tasks");
preloadTaskTemplates();

return (
@@ -34,12 +35,14 @@ const TaskTemplates: React.FC = async () => {
LinkComponent={Link}
href="/tasks/create"
>
{t("Create Template")}
{t("Create Task Template")}
</Button>
</Stack>
<Suspense fallback={<TaskTemplateSearch.Loading />}>
<TaskTemplateSearch />
</Suspense>
<I18nProvider namespaces={["tasks", "common"]}>
<Suspense fallback={<TaskTemplateSearch.Loading />}>
<TaskTemplateSearch />
</Suspense>
</I18nProvider>
</>
);
};


+ 1
- 1
src/app/api/claims/actions.ts Bestand weergeven

@@ -21,7 +21,7 @@ export interface ClaimDetailTable {
id: number;
invoiceDate: Date;
description: string;
project: ProjectCombo;
project: number;
amount: number;
supportingDocumentName: string;
oldSupportingDocument: SupportingDocument;


+ 3
- 3
src/app/api/clientprojects/index.ts Bestand weergeven

@@ -27,7 +27,7 @@ const mockProjects: ClientProjectResult[] = [
NoOfProjects: 5,
},
{
id: 1,
id: 2,
clientCode: "CUST-001",
clientName: "Client A",
SubsidiaryClientCode: "SUBS-001",
@@ -35,7 +35,7 @@ const mockProjects: ClientProjectResult[] = [
NoOfProjects: 5,
},
{
id: 1,
id: 3,
clientCode: "CUST-001",
clientName: "Client A",
SubsidiaryClientCode: "SUBS-002",
@@ -43,7 +43,7 @@ const mockProjects: ClientProjectResult[] = [
NoOfProjects: 3,
},
{
id: 1,
id: 4,
clientCode: "CUST-001",
clientName: "Client A",
SubsidiaryClientCode: "SUBS-003",


+ 31
- 3
src/app/api/projects/actions.ts Bestand weergeven

@@ -1,17 +1,22 @@
"use server";

import { serverFetchJson } from "@/app/utils/fetchUtil";
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { Task, TaskGroup } from "../tasks";
import { Customer } from "../customer";
import { revalidateTag } from "next/cache";

export interface CreateProjectInputs {
// Project details
// Project
projectId: number | null;
projectDeleted: boolean | null;
projectCode: string;
projectName: string;
projectCategoryId: number;
projectDescription: string;
projectLeadId: number;
projectActualStart: string;
projectActualEnd: string;

// Project info
serviceTypeId: number;
@@ -61,10 +66,33 @@ export interface PaymentInputs {
amount: number;
}

export interface CreateProjectResponse {
id: number,
name: string,
code: string,
category: string,
team: string,
client: string,
}
export const saveProject = async (data: CreateProjectInputs) => {
return serverFetchJson(`${BASE_API_URL}/projects/new`, {
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" },
},
);

return project
};

+ 26
- 2
src/app/api/projects/index.ts Bestand weergeven

@@ -3,6 +3,7 @@ import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import "server-only";
import { Task, TaskGroup } from "../tasks";
import { CreateProjectInputs } from "./actions";

export interface ProjectResult {
id: number;
@@ -55,10 +56,15 @@ export interface AssignedProject {
tasks: Task[];
milestones: {
[taskGroupId: TaskGroup["id"]]: {
startDate: string;
endDate: string;
startDate?: string;
endDate?: string;
};
};
// Manhour info
hoursSpent: number;
hoursSpentOther: number;
hoursAllocated: number;
hoursAllocatedOther: number;
}

export const preloadProjects = () => {
@@ -131,3 +137,21 @@ export const fetchProjectWorkNatures = cache(async () => {
next: { tags: ["projectWorkNatures"] },
});
});

export const fetchAssignedProjects = cache(async () => {
return serverFetchJson<AssignedProject[]>(
`${BASE_API_URL}/projects/assignedProjects`,
{
next: { tags: ["assignedProjects"] },
},
);
});

export const fetchProjectDetails = cache(async (projectId: string) => {
return serverFetchJson<CreateProjectInputs>(
`${BASE_API_URL}/projects/projectDetails/${projectId}`,
{
next: { tags: [`projectDetails_${projectId}`] },
},
);
});

+ 42
- 0
src/app/api/report7/index.ts Bestand weergeven

@@ -0,0 +1,42 @@
//src\app\api\report\index.ts
import { cache } from "react";

export interface ProjectClaims {
id: number;
projectCode: string;
projectName: string;
team: string;
teamLeader: string;
startDate: string;
startDateFrom: string;
startDateTo: string;
targetEndDate: string;
client: string;
subsidiary: string;
staffName: string;
}

export const preloadProjects = () => {
fetchProjectsProjectClaims();
};

export const fetchProjectsProjectClaims = cache(async () => {
return mockProjects;
});

const mockProjects: ProjectClaims[] = [
{
id: 1,
projectCode: "CUST-001",
projectName: "Client A",
team: "N/A",
teamLeader: "N/A",
startDate: "1/2/2024",
startDateFrom: "1/2/2024",
startDateTo: "1/2/2024",
targetEndDate: "30/3/2024",
client: "ss",
subsidiary: "sus",
staffName: "Leo",
},
];

+ 42
- 0
src/app/api/reporte1/index.ts Bestand weergeven

@@ -0,0 +1,42 @@
//src\app\api\report\index.ts
import { cache } from "react";

export interface FinancialStatus {
id: number;
projectCode: string;
projectName: string;
team: string;
teamLeader: string;
startDate: string;
startDateFrom: string;
startDateTo: string;
targetEndDate: string;
client: string;
subsidiary: string;
status: string;
}

export const preloadProjects = () => {
fetchProjectsFinancialStatus();
};

export const fetchProjectsFinancialStatus = cache(async () => {
return mockProjects;
});

const mockProjects: FinancialStatus[] = [
{
id: 1,
projectCode: "CUST-001",
projectName: "Client A",
team: "N/A",
teamLeader: "N/A",
startDate: "5",
startDateFrom: "5",
startDateTo: "5",
targetEndDate: "s",
client: "ss",
subsidiary: "ss",
status: "1",
},
];

+ 23
- 0
src/app/api/reports/actions.ts Bestand weergeven

@@ -0,0 +1,23 @@
"use server";

import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil";
import { EX02ProjectCashFlowReportRequest } from ".";
import { BASE_API_URL } from "@/config/api";

export interface FileResponse {
filename: string;
blobValue: Uint8Array;
}

export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowReportRequest) => {
const reportBlob = await serverFetchBlob<FileResponse>(
`${BASE_API_URL}/reports/EX02-ProjectCashFlowReport`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);

return reportBlob
};

+ 8
- 0
src/app/api/reports/index.ts Bestand weergeven

@@ -0,0 +1,8 @@
// EX02 - Project Cash Flow Report
export interface EX02ProjectCashFlowReportFilter {
project: string[];
}

export interface EX02ProjectCashFlowReportRequest {
projectId: number;
}

+ 53
- 0
src/app/api/resourcesummary/index.ts Bestand weergeven

@@ -0,0 +1,53 @@
import { cache } from "react";

export interface ResourceSummaryResult {
id: number;
projectCode: string;
projectName: string;
clientCode: string;
clientName: string;
clientCodeAndName: string;
}

export const preloadProjects = () => {
fetchResourceSummary();
};

export const fetchResourceSummary = cache(async () => {
return mockProjects;
});

const mockProjects: ResourceSummaryResult[] = [
{
id: 1,
projectCode: 'C-1001-001',
projectName: 'Consultancy Project A',
clientCode: 'Client-001',
clientName: 'AAA Construction',
clientCodeAndName: 'Client-001 - AAA Construction',
},
{
id: 2,
projectCode: 'C-1002-001',
projectName: 'Consultancy Project B',
clientCode: 'Client-001',
clientName: 'AAA Construction',
clientCodeAndName: 'Client-001 - AAA Construction',
},
{
id: 3,
projectCode: 'C-1003-001',
projectName: 'Consultancy Project C',
clientCode: 'Client-002',
clientName: 'BBB Construction',
clientCodeAndName: 'Client-002 - BBB Construction',
},
{
id: 4,
projectCode: 'C-1004-001',
projectName: 'Consultancy Project D',
clientCode: 'Client-002',
clientName: 'BBB Construction',
clientCodeAndName: 'Client-002 - BBB Construction',
},
];

+ 17
- 1
src/app/api/skill/actions.ts Bestand weergeven

@@ -5,6 +5,13 @@ import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";

export interface CreateSkillInputs {
id?: number;
name: String;
code: String;
description: String;
}

export interface comboProp {
id: any;
label: string;
@@ -18,4 +25,13 @@ export const fetchSkillCombo = cache(async () => {
return serverFetchJson<combo>(`${BASE_API_URL}/skill/combo`, {
next: { tags: ["skill"] },
});
});
});

export const saveSkill = async (data: CreateSkillInputs) => {
return serverFetchJson(`${BASE_API_URL}/skill/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

+ 22
- 0
src/app/api/skill/index.ts Bestand weergeven

@@ -0,0 +1,22 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import "server-only";

export interface SkillResult {
action: any;
id: number;
name: string;
description: string;
code: string;
}

export const preloadSkill = () => {
fetchSkill();
};

export const fetchSkill = cache(async () => {
return serverFetchJson<SkillResult[]>(`${BASE_API_URL}/skill`, {
next: { tags: ["sill"] },
});
});

+ 3
- 3
src/app/api/staff/actions.ts Bestand weergeven

@@ -1,5 +1,5 @@
"use server";
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { StaffResult, data } from ".";
import { cache } from "react";
@@ -59,8 +59,8 @@ export const testing = async (data: CreateStaffInputs) => {
});
};
export const deleteStaff = async (data: StaffResult) => {
return serverFetchJson(`${BASE_API_URL}/staffs/delete/${data.id}`, {
export const deleteStaff = async (id: number) => {
return serverFetchWithNoContent(`${BASE_API_URL}/staffs/delete/${id}`, {
method: "DELETE",
// body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },


+ 28
- 2
src/app/api/tasks/actions.ts Bestand weergeven

@@ -1,6 +1,6 @@
"use server";

import { serverFetchJson } from "@/app/utils/fetchUtil";
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { TaskTemplate } from ".";
import { revalidateTag } from "next/cache";
@@ -9,11 +9,13 @@ export interface NewTaskTemplateFormInputs {
code: string;
name: string;
taskIds: number[];

id: number | null;
}

export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => {
const newTaskTemplate = await serverFetchJson<TaskTemplate>(
`${BASE_API_URL}/tasks/templates/new`,
`${BASE_API_URL}/tasks/templates/save`,
{
method: "POST",
body: JSON.stringify(data),
@@ -25,3 +27,27 @@ export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => {

return newTaskTemplate;
};

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

return taskTemplate;
};

export const deleteTaskTemplate = async (id: number) => {
const taskTemplate = await serverFetchWithNoContent(
`${BASE_API_URL}/tasks/templates/${id}`,
{
method: "DELETE",
headers: { "Content-Type": "application/json" },
},
);

return taskTemplate
};

+ 3
- 4
src/app/api/team/actions.ts Bestand weergeven

@@ -1,5 +1,5 @@
"use server";
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import { TeamResult } from ".";
@@ -53,10 +53,9 @@ export const saveTeam = async (data: CreateTeamInputs) => {
};

export const deleteTeam = async (data: TeamResult) => {
return serverFetchJson(`${BASE_API_URL}/team/delete/${data.id}`, {
export const deleteTeam = async (id: number) => {
return serverFetchWithNoContent(`${BASE_API_URL}/team/delete/${id}`, {
method: "DELETE",
// body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

+ 1
- 0
src/app/api/team/index.ts Bestand weergeven

@@ -15,6 +15,7 @@ export interface TeamResult {
staffName: string;
posLabel: string;
posCode: string;
teamLead: number;

}


+ 27
- 0
src/app/api/user/actions.ts Bestand weergeven

@@ -0,0 +1,27 @@
"use server";

import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";
import { UserDetail, UserResult } from ".";
import { cache } from "react";

export interface UserInputs {
username: string;
firstname: string;
lastname: string;
}


export const fetchUserDetails = cache(async (id: number) => {
return serverFetchJson<UserDetail>(`${BASE_API_URL}/user/${id}`, {
next: { tags: ["user"] },
});
});

export const deleteUser = async (id: number) => {
return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
};

+ 43
- 0
src/app/api/user/index.ts Bestand weergeven

@@ -0,0 +1,43 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import "server-only";


export interface UserResult {
action: any;
id: number;
name: string;
locale: string;
username: string;
fullName: string;
firstname: string;
lastname: string;
title: string;
department: string;
email: string;
phone1: string;
phone2: string;
remarks: string;
}

// export interface DetailedUser extends UserResult {
// username: string;
// password: string
// }

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

export const preloadUser = () => {
fetchUser();
};

export const fetchUser = cache(async () => {
return serverFetchJson<UserResult[]>(`${BASE_API_URL}/user`, {
next: { tags: ["user"] },
});
});

+ 8
- 0
src/app/utils/commonUtil.ts Bestand weergeven

@@ -20,4 +20,12 @@ export const dateInRange = (currentDate: string, startDate: string, endDate: str
return true
}
}
}

export const downloadFile = (blobData: Uint8Array, filename: string) => {
const url = URL.createObjectURL(new Blob([blobData]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
link.click();
}

+ 91
- 2
src/app/utils/fetchUtil.ts Bestand weergeven

@@ -3,11 +3,22 @@ import { getServerSession } from "next-auth";
import { headers } from "next/headers";
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) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const session = await getServerSession<any, SessionWithTokens>(authOptions);
const accessToken = session?.accessToken;

console.log(accessToken);
return fetch(input, {
...init,
headers: {
@@ -15,7 +26,8 @@ export const serverFetch: typeof fetch = async (input, init) => {
...(accessToken
? {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
Accept:
"application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}
: {}),
},
@@ -35,7 +47,10 @@ export async function serverFetchJson<T>(...args: FetchParams) {
signOutUser();
default:
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,
);
}
}
}
@@ -56,6 +71,80 @@ export async function serverFetchWithNoContent(...args: FetchParams) {
}
}

export async function serverFetchBlob<T>(...args: FetchParams) {
const response = await serverFetch(...args);

if (response.ok) {
const body = response.body;
// console.log(body)
// console.log(body?.tee()[0].getReader())

const reader = body?.getReader();
let finalUInt8Array = new Uint8Array();
let done = false;

while (!done) {
const read = await reader?.read();

// version 1
if (read?.done) {
done = true;
} else {
const tempUInt8Array = new Uint8Array(
finalUInt8Array.length + read?.value.length!,
);
tempUInt8Array.set(finalUInt8Array);
tempUInt8Array.set(read?.value!, finalUInt8Array.length);
finalUInt8Array = new Uint8Array(tempUInt8Array.length!);
finalUInt8Array.set(tempUInt8Array);

// console.log("1", finalUInt8Array)
}
}

// version 2 & return bodyRead
// const bodyRead = reader?.read().then(function processText({ done, value }): any {
// // Result objects contain two properties:
// // done - true if the stream has already given you all its data.
// // value - some data. Always undefined when done is true.
// if (done) {
// console.log("Stream complete");
// return { filename: response.headers.get("filename"), blobValue: finalUInt8Array } as T;;
// }

// // value for fetch streams is a Uint8Array
// finalUInt8Array = new Uint8Array(value.length)
// finalUInt8Array.set(value)

// console.log(finalUInt8Array)
// // Read some more, and call this function again
// return reader.read().then(processText);
// })
// const bodyValue = bodyRead?.value

// const blob = await response.blob()
// const blobText = await blob.text();
// const blobType = await blob.type;

// console.log(bodyReader)
// console.log("2", finalUInt8Array)
// console.log(bodyValue)

return {
filename: response.headers.get("filename"),
blobValue: finalUInt8Array,
} as T;
} else {
switch (response.status) {
case 401:
signOutUser();
default:
console.error(await response.text());
throw Error("Something went wrong fetching data in server.");
}
}
}

export const signOutUser = () => {
const headersList = headers();
const referer = headersList.get("referer");


+ 2
- 0
src/components/Breadcrumb/Breadcrumb.tsx Bestand weergeven

@@ -12,6 +12,7 @@ const pathToLabelMap: { [path: string]: string } = {
"/home": "User Workspace",
"/projects": "Projects",
"/projects/create": "Create Project",
"/projects/edit": "Edit Project",
"/tasks": "Task Template",
"/tasks/create": "Create Task Template",
"/staffReimbursement": "Staff Reimbursement",
@@ -28,6 +29,7 @@ const pathToLabelMap: { [path: string]: string } = {
"/settings/position": "Position",
"/settings/position/new": "Create Position",
"/settings/salarys": "Salary",
"/analytics/EX02ProjectCashFlowReport": "EX02 - Project Cash Flow Report",
};

const Breadcrumb = () => {


+ 0
- 1
src/components/ClaimDetail/index.ts Bestand weergeven

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

src/components/ClaimDetail/ClaimFormInfo.tsx → src/components/ClaimSave/ClaimFormInfo.tsx Bestand weergeven


src/components/ClaimDetail/ClaimFormInputGrid.tsx → src/components/ClaimSave/ClaimFormInputGrid.tsx Bestand weergeven

@@ -371,20 +371,21 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({
flex: 1,
editable: true,
type: "singleSelect",
getOptionLabel: (value: any) => {
getOptionLabel: (value: ProjectCombo) => {
return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`;
},
getOptionValue: (value: any) => value,
getOptionValue: (value: ProjectCombo) => value.id,
valueOptions: () => {
const options = projectCombo ?? []

if (options.length === 0) {
options.push({ id: -1, code: "", name: "No Projects" })
}
return options;

return options as ProjectCombo[];
},
valueGetter: (params) => {
return params.value ?? projectCombo[0].id ?? -1
return params.value ?? projectCombo[0] ?? { id: -1, code: "", name: "No Projects" } as ProjectCombo
},
},
{

src/components/ClaimDetail/ClaimDetail.tsx → src/components/ClaimSave/ClaimSave.tsx Bestand weergeven

@@ -21,7 +21,7 @@ export interface Props {
projectCombo: ProjectCombo[]
}

const ClaimDetail: React.FC<Props> = ({ projectCombo }) => {
const ClaimSave: React.FC<Props> = ({ projectCombo }) => {
const { t } = useTranslation("common");
const [serverError, setServerError] = useState("");
const router = useRouter();
@@ -74,14 +74,15 @@ const ClaimDetail: React.FC<Props> = ({ projectCombo }) => {
const buttonName = (event?.nativeEvent as any).submitter.name
const formData = new FormData()
formData.append("expenseType", data.expenseType)
data.addClaimDetails.forEach((claimDetail) => {
formData.append("addClaimDetailIds", JSON.stringify(claimDetail.id))
formData.append("addClaimDetailInvoiceDates", convertDateToString(claimDetail.invoiceDate, "YYYY-MM-DD"))
formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project.id))
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++) {
// const updatedData = {
@@ -154,4 +155,4 @@ const ClaimDetail: React.FC<Props> = ({ projectCombo }) => {
);
};

export default ClaimDetail;
export default ClaimSave;

src/components/ClaimDetail/ClaimDetailWrapper.tsx → src/components/ClaimSave/ClaimSaveWrapper.tsx Bestand weergeven

@@ -1,6 +1,6 @@

import React from "react";
import ClaimDetail from "./ClaimDetail";
import ClaimSave from "./ClaimSave";
import { fetchProjectCombo } from "@/app/api/claims";
// import TaskSetup from "./TaskSetup";
// import StaffAllocation from "./StaffAllocation";
@@ -13,7 +13,7 @@ const ClaimDetailWrapper: React.FC = async () => {
]);

return (
<ClaimDetail projectCombo={projectCombo}/>
<ClaimSave projectCombo={projectCombo}/>
);
};


+ 1
- 0
src/components/ClaimSave/index.ts Bestand weergeven

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

+ 6
- 6
src/components/ClaimSearch/ClaimSearch.tsx Bestand weergeven

@@ -50,12 +50,12 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => {

const columns = useMemo<Column<Claim>[]>(
() => [
// {
// name: "action",
// label: t("Actions"),
// onClick: onClaimClick,
// buttonIcon: <EditNote />,
// },
{
name: "id",
label: t("Details"),
onClick: onClaimClick,
buttonIcon: <EditNote />,
},
{ name: "created", label: t("Creation Date"), type: "date" },
{ name: "code", label: t("Claim Code") },
// { name: "project", label: t("Related Project Name") },


+ 163
- 78
src/components/CreateProject/CreateProject.tsx Bestand weergeven

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

import DoneIcon from '@mui/icons-material/Done'
import Check from "@mui/icons-material/Check";
import Close from "@mui/icons-material/Close";
import Button from "@mui/material/Button";
@@ -21,8 +22,8 @@ import {
SubmitHandler,
useForm,
} 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 {
BuildingType,
ContractType,
@@ -36,8 +37,13 @@ import { StaffResult } from "@/app/api/staff";
import { Typography } from "@mui/material";
import { Grade } from "@/app/api/grades";
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 {
isEditMode: boolean;
defaultInputs?: CreateProjectInputs;
allTasks: Task[];
projectCategories: ProjectCategory[];
taskTemplates: TaskTemplate[];
@@ -69,6 +75,8 @@ const hasErrorsInTab = (
};

const CreateProject: React.FC<Props> = ({
isEditMode,
defaultInputs,
allTasks,
projectCategories,
taskTemplates,
@@ -90,9 +98,22 @@ const CreateProject: React.FC<Props> = ({
const router = useRouter();

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"]>>(
(_e, newValue) => {
setTabIndex(newValue);
@@ -101,16 +122,53 @@ const CreateProject: React.FC<Props> = ({
);

const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>(
async (data) => {
async (data, event) => {
try {
console.log("first")
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) {
setServerError(t("An error has occurred. Please try again later."));
}
},
[router, t],
[router, t, isEditMode],
);

const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>(
@@ -133,85 +191,112 @@ const CreateProject: React.FC<Props> = ({
allocatedStaffIds: [],
milestones: {},
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]: 1 / grades.length };
}, {})
: defaultInputs?.manhourPercentageByGrade,
},
});

const errors = formProps.formState.errors;

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")}
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}
>
{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>
</FormProvider>
</FormProvider>
</>
);
};



+ 16
- 1
src/components/CreateProject/CreateProjectWrapper.tsx Bestand weergeven

@@ -4,6 +4,7 @@ import {
fetchProjectBuildingTypes,
fetchProjectCategories,
fetchProjectContractTypes,
fetchProjectDetails,
fetchProjectFundingTypes,
fetchProjectLocationTypes,
fetchProjectServiceTypes,
@@ -13,7 +14,15 @@ import { fetchStaff, fetchTeamLeads } from "@/app/api/staff";
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer";
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 [
tasks,
taskTemplates,
@@ -46,8 +55,14 @@ const CreateProjectWrapper: React.FC = async () => {
fetchGrades(),
]);

const projectInfo = props.isEditMode
? await fetchProjectDetails(props.projectId)
: undefined;

return (
<CreateProject
isEditMode={props.isEditMode}
defaultInputs={projectInfo}
allTasks={tasks}
projectCategories={projectCategories}
taskTemplates={taskTemplates}


+ 122
- 0
src/components/CreateSkill/CreateSkill.tsx Bestand weergeven

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

import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
} from "react-hook-form";
import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material";
import { Check, Close, RestartAlt } from "@mui/icons-material";
import { useCallback, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslation } from "react-i18next";
import { CreateSkillInputs, saveSkill } from "@/app/api/skill/actions";
import { Error } from "@mui/icons-material";
import SkillInfo from "./SkillInfo";

interface Props {}

const CreateSkill: React.FC<Props> = () => {
const formProps = useForm<CreateSkillInputs>();
const [serverError, setServerError] = useState("");
const router = useRouter();
const { t } = useTranslation();
const [tabIndex, setTabIndex] = useState(0);
const errors = formProps.formState.errors;

const onSubmit = useCallback<SubmitHandler<CreateSkillInputs>>(
async (data) => {
try {
console.log(data);
await saveSkill(data)
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 handleReset = useCallback(() => {
// console.log(defaultValues)
// }, [defaultValues])

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

const hasErrorsInTab = (
tabIndex: number,
errors: FieldErrors<CreateSkillInputs>
) => {
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("Team Info")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end"
/>
{/* <Tab label={t("Certification")} iconPosition="end" /> */}
</Tabs>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
{tabIndex === 0 && <SkillInfo />}
<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 CreateSkill;

+ 40
- 0
src/components/CreateSkill/CreateSkillLoading.tsx Bestand weergeven

@@ -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 CreateSkillLoading: 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>CreateSkill
<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 CreateSkillLoading;

+ 19
- 0
src/components/CreateSkill/CreateSkillWrapper.tsx Bestand weergeven

@@ -0,0 +1,19 @@
import React from "react";
import CreateSkill from "./CreateSkill";
import CreateSkillLoading from "./CreateSkillLoading";
import { fetchStaff, fetchTeamLeads } from "@/app/api/staff";
import { useSearchParams } from "next/navigation";

interface SubComponents {
Loading: typeof CreateSkillLoading;
}

const CreateSkillWrapper: React.FC & SubComponents = async () => {


return <CreateSkill/>;
};

CreateSkillWrapper.Loading = CreateSkillLoading;

export default CreateSkillWrapper;

+ 90
- 0
src/components/CreateSkill/SkillInfo.tsx Bestand weergeven

@@ -0,0 +1,90 @@
"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 { useTranslation } from "react-i18next";
import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Button from "@mui/material/Button";
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
import { useCallback } from "react";
import { CreateSkillInputs } from "@/app/api/skill/actions";

const SkillInfo: React.FC = (
) => {
const { t } = useTranslation();
const {
register,
formState: { errors, defaultValues },
control,
reset,
resetField,
setValue,
} = useFormContext<CreateSkillInputs>();

const resetSkill = useCallback(() => {
console.log(defaultValues);
if (defaultValues !== undefined) {
resetField("name");
}
}, [defaultValues]);
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 SkillInfo;

+ 1
- 0
src/components/CreateSkill/index.ts Bestand weergeven

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

+ 1
- 1
src/components/CreateStaff/CreateStaff.tsx Bestand weergeven

@@ -190,7 +190,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => {
{
id: "skillSetId",
label: t("Skillset"),
type: "combo-Obj",
type: "multiSelect-Obj",
options: skillCombo || [],
required: false,
},


+ 122
- 69
src/components/CreateTaskTemplate/CreateTaskTemplate.tsx Bestand weergeven

@@ -10,15 +10,17 @@ import TransferList from "../TransferList";
import Button from "@mui/material/Button";
import Check from "@mui/icons-material/Check";
import Close from "@mui/icons-material/Close";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import React from "react";
import Stack from "@mui/material/Stack";
import { Task } from "@/app/api/tasks";
import {
NewTaskTemplateFormInputs,
fetchTaskTemplate,
saveTaskTemplate,
} from "@/app/api/tasks/actions";
import { SubmitHandler, useForm } from "react-hook-form";
import { SubmitHandler, useFieldArray, useForm } from "react-hook-form";
import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts";

interface Props {
tasks: Task[];
@@ -27,6 +29,7 @@ interface Props {
const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => {
const { t } = useTranslation();

const searchParams = useSearchParams()
const router = useRouter();
const handleCancel = () => {
router.back();
@@ -49,6 +52,7 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => {
handleSubmit,
setValue,
watch,
resetField,
formState: { errors, isSubmitting },
} = useForm<NewTaskTemplateFormInputs>({ defaultValues: { taskIds: [] } });

@@ -57,12 +61,56 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => {
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 onSubmit: SubmitHandler<NewTaskTemplateFormInputs> = React.useCallback(
async (data) => {
try {
setServerError("");
await saveTaskTemplate(data);
router.replace("/tasks");
submitDialog(async () => {
const response = await saveTaskTemplate(data);

if (response?.id !== null && response?.id !== undefined && response?.id > 0) {
successDialog(t("Submit Success"), t).then(() => {
router.replace("/tasks");
})
} else {
errorDialog(t("Submit Fail"), t).then(() => {
return false
})
}
}, t)
} catch (e) {
setServerError(t("An error has occurred. Please try again later."));
}
@@ -71,72 +119,77 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => {
);

return (
<Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}>
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Task List Setup")}</Typography>
<Grid
container
spacing={2}
columns={{ xs: 6, sm: 12 }}
marginBlockEnd={1}
>
<Grid item xs={6}>
<TextField
label={t("Task Template Code")}
fullWidth
{...register("code", {
required: t("Task template code is required"),
})}
error={Boolean(errors.code?.message)}
helperText={errors.code?.message}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Task Template Name")}
fullWidth
{...register("name", {
required: t("Task template name is required"),
})}
error={Boolean(errors.name?.message)}
helperText={errors.name?.message}
<>
{
(id === null || refTaskTemplate !== undefined) && <Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}>
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Task List Setup")}</Typography>
<Grid
container
spacing={2}
columns={{ xs: 6, sm: 12 }}
marginBlockEnd={1}
>
<Grid item xs={6}>
<TextField
label={t("Task Template Code")}
fullWidth
{...register("code", {
required: t("Task template code is required"),
})}
error={Boolean(errors.code?.message)}
helperText={errors.code?.message}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Task Template Name")}
fullWidth
{...register("name", {
required: t("Task template name is required"),
})}
error={Boolean(errors.name?.message)}
helperText={errors.name?.message}
/>
</Grid>
</Grid>
<TransferList
allItems={items}
selectedItems={selectedItems}
onChange={(selectedTasks) => {
setValue(
"taskIds",
selectedTasks.map((item) => item.id),
);
}}
allItemsLabel={t("Task Pool")}
selectedItemsLabel={t("Task List Template")}
/>
</Grid>
</Grid>
<TransferList
allItems={items}
selectedItems={selectedItems}
onChange={(selectedTasks) => {
setValue(
"taskIds",
selectedTasks.map((item) => item.id),
);
}}
allItemsLabel={t("Task Pool")}
selectedItemsLabel={t("Task List Template")}
/>
</CardContent>
</Card>
{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={isSubmitting}
>
{t("Confirm")}
</Button>
</Stack>
</Stack>
</CardContent>
</Card>
{
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={isSubmitting}
>
{t("Confirm")}
</Button>
</Stack>
</Stack >}
</>
);
};



+ 1
- 1
src/components/CreateTeam/CreateTeam.tsx Bestand weergeven

@@ -89,7 +89,7 @@ const hasErrorsInTab = (
}
iconPosition="end"
/>
<Tab label={t("Subsidiary Allocation")} iconPosition="end" />
<Tab label={t("Staff Allocation")} iconPosition="end" />
</Tabs>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">


+ 94
- 80
src/components/CreateTeam/StaffAllocation.tsx Bestand weergeven

@@ -18,9 +18,21 @@ import { StaffResult } from "@/app/api/staff";
import SearchResults, { Column } from "../SearchResults";
import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material";
import { Card } from "reactstrap";
import { Box, CardContent, Grid, IconButton, InputAdornment, Stack, Tab, Tabs, TabsProps, TextField, Typography } from "@mui/material";
import {
Box,
CardContent,
Grid,
IconButton,
InputAdornment,
Stack,
Tab,
Tabs,
TabsProps,
TextField,
Typography,
} from "@mui/material";
import { differenceBy } from "lodash";
import StarsIcon from '@mui/icons-material/Stars';
import StarsIcon from "@mui/icons-material/Stars";

export interface Props {
allStaffs: StaffResult[];
@@ -35,16 +47,15 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
reset,
resetField,
} = useFormContext<CreateTeamInputs>();
const initialStaffs = staff.map((s) => ({ ...s }));
// console.log(initialStaffs)
// console.log(initialStaffs)
const [filteredStaff, setFilteredStaff] = useState(initialStaffs);
const [selectedStaff, setSelectedStaff] = useState<typeof filteredStaff>(
initialStaffs.filter((s) => getValues("addStaffIds")?.includes(s.id))
);
const [seletedTeamLead, setSeletedTeamLead] = useState<number>()
// Adding / Removing staff

// Adding / Removing staff
const addStaff = useCallback((staff: StaffResult) => {
setSelectedStaff((s) => [...s, staff]);
}, []);
@@ -53,27 +64,31 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
setSelectedStaff((s) => s.filter((s) => s.id !== staff.id));
}, []);

const setTeamLead = useCallback((staff: StaffResult) => {
setSeletedTeamLead(staff.id)
const rearrangedList = getValues("addStaffIds").reduce<number[]>((acc, num, index) => {
if (num === staff.id && index !== 0) {
const setTeamLead = useCallback(
(staff: StaffResult) => {
const rearrangedList = getValues("addStaffIds").reduce<number[]>(
(acc, num, index) => {
if (num === staff.id && index !== 0) {
acc.splice(index, 1);
acc.unshift(num)
}
return acc;
}, getValues("addStaffIds"));
console.log(rearrangedList)
console.log(selectedStaff)

const rearrangedStaff = rearrangedList.map((id) => {
acc.unshift(num);
}
return acc;
},
getValues("addStaffIds")
);
console.log(rearrangedList);
console.log(selectedStaff);

const rearrangedStaff = rearrangedList.map((id) => {
return selectedStaff.find((staff) => staff.id === id);
});
console.log(rearrangedStaff)
setSelectedStaff(rearrangedStaff as StaffResult[]);
console.log(rearrangedStaff);
setSelectedStaff(rearrangedStaff as StaffResult[]);

setValue("addStaffIds", rearrangedList)
}, [addStaff, selectedStaff]);
setValue("addStaffIds", rearrangedList);
},
[addStaff, selectedStaff]
);

const clearSubsidiary = useCallback(() => {
if (defaultValues !== undefined) {
@@ -86,7 +101,7 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {

// Sync with form
useEffect(() => {
console.log(selectedStaff)
console.log(selectedStaff);
setValue(
"addStaffIds",
selectedStaff.map((s) => s.id)
@@ -94,7 +109,7 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
}, [selectedStaff, setValue]);

useEffect(() => {
console.log(selectedStaff)
console.log(selectedStaff);
}, [selectedStaff]);

const StaffPoolColumns = useMemo<Column<StaffResult>[]>(
@@ -107,7 +122,7 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
},
{ label: t("Staff Id"), name: "staffId" },
{ label: t("Staff Name"), name: "name" },
{ label: t("Current Position"), name: "currentPosition" },
{ label: t("Position"), name: "currentPosition" },
],
[addStaff, t]
);
@@ -122,7 +137,7 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
},
{ label: t("Staff Id"), name: "staffId" },
{ label: t("Staff Name"), name: "name" },
{ label: t("Current Position"), name: "currentPosition" },
{ label: t("Position"), name: "currentPosition" },
{
label: t("Team Lead"),
name: "action",
@@ -144,16 +159,16 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
}, []);

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))
// })
// );
setFilteredStaff(
initialStaffs.filter((i) => {
const q = query.toLowerCase();
return (
i.staffId.toLowerCase().includes(q) ||
i.name.toLowerCase().includes(q) ||
i.currentPosition.toLowerCase().includes(q)
);
})
);
}, [staff, query]);

const resetStaff = React.useCallback(() => {
@@ -161,8 +176,7 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
clearSubsidiary();
}, [clearQueryInput, clearSubsidiary]);

const formProps = useForm({
});
const formProps = useForm({});

// Tab related
const [tabIndex, setTabIndex] = React.useState(0);
@@ -170,7 +184,7 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
(_e, newValue) => {
setTabIndex(newValue);
},
[],
[]
);

return (
@@ -185,48 +199,48 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
{t("staff")}
</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 subsidiary code, name or br no.")}
InputProps={{
endAdornment: query && (
<InputAdornment position="end">
<IconButton onClick={clearQueryInput}>
<Clear />
</IconButton>
</InputAdornment>
),
}}
/>
<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>
</Grid>
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("Staff Pool")} />
<Tab
label={`${t("Allocated Staff")} (${selectedStaff.length})`}
/>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredStaff, selectedStaff, "id")}
columns={StaffPoolColumns}
/>
)}
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedStaff}
columns={allocatedStaffColumns}
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("Staff Pool")} />
<Tab
label={`${t("Allocated Staff")} (${selectedStaff.length})`}
/>
)}
</Box>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredStaff, selectedStaff, "id")}
columns={StaffPoolColumns}
/>
)}
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedStaff}
columns={allocatedStaffColumns}
/>
)}
</Box>
</Stack>
</CardContent>
</Card>


+ 15
- 3
src/components/CustomDatagrid/CustomDatagrid.tsx Bestand weergeven

@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import { Card, CardHeader, CardContent, SxProps, Theme } from "@mui/material";
import { DataGrid, GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
import { DataGrid, GridColDef, GridRowSelectionModel, GridColumnGroupingModel} from "@mui/x-data-grid";
import { darken, lighten, styled } from "@mui/material/styles";
import { useState } from "react";

@@ -19,6 +19,8 @@ interface CustomDatagridProps {
newSelectionModel: GridRowSelectionModel,
) => void;
selectionModel?: any;
columnGroupingModel?: any;
pageSize?:any;
}

const CustomDatagrid: React.FC<CustomDatagridProps> = ({
@@ -32,6 +34,8 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
checkboxSelection, // Destructure the new prop
onRowSelectionModelChange, // Destructure the new prop
selectionModel,
columnGroupingModel,
pageSize,
...props
}) => {
const modifiedColumns = columns.map((column) => {
@@ -193,6 +197,8 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
editMode="row"
checkboxSelection={checkboxSelection}
onRowSelectionModelChange={onRowSelectionModelChange}
experimentalFeatures={{ columnGrouping: true }}
columnGroupingModel={columnGroupingModel}
initialState={{
pagination: { paginationModel: { pageSize: 10 } },
}}
@@ -222,6 +228,8 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
editMode="row"
checkboxSelection={checkboxSelection}
onRowSelectionModelChange={onRowSelectionModelChange}
experimentalFeatures={{ columnGrouping: true }}
columnGroupingModel={columnGroupingModel}
initialState={{
pagination: { paginationModel: { pageSize: 10 } },
}}
@@ -251,6 +259,8 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
editMode="row"
checkboxSelection={checkboxSelection}
onRowSelectionModelChange={onRowSelectionModelChange}
experimentalFeatures={{ columnGrouping: true }}
columnGroupingModel={columnGroupingModel}
style={{ marginRight: 20 }}
initialState={{
pagination: { paginationModel: { pageSize: 10 } },
@@ -282,8 +292,10 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
style={{ marginRight: 0 }}
checkboxSelection={checkboxSelection}
onRowSelectionModelChange={onRowSelectionModelChange}
experimentalFeatures={{ columnGrouping: true }}
columnGroupingModel={columnGroupingModel}
initialState={{
pagination: { paginationModel: { pageSize: 10 } },
pagination: { paginationModel: { pageSize: pageSize ?? 10 } },
}}
className="customDataGrid"
sx={{
@@ -293,7 +305,7 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
"& .MuiDataGrid-cell:hover": {
color: "primary.main",
},
height: 300,
height: dataGridHeight ?? 300,
"& .MuiDataGrid-root": {
overflow: "auto",
},


+ 65
- 3
src/components/CustomInputForm/CustomInputForm.tsx Bestand weergeven

@@ -15,6 +15,7 @@ import {
Checkbox,
FormControlLabel,
Button,
Chip,
} from "@mui/material";
import { DataGrid, GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
import { darken, lighten, styled } from "@mui/material/styles";
@@ -31,6 +32,7 @@ import { useCallback, useEffect, useState } from "react";
import { Check, Close, RestartAlt } from "@mui/icons-material";
import { NumericFormat, NumericFormatProps } from "react-number-format";
import * as React from "react";
import CancelIcon from "@mui/icons-material/Cancel";

interface Options {
id: any;
@@ -286,7 +288,7 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({
</Grid>
);
} else if (field.type === "multiDate") {
console.log(dayjs(field.value))
// console.log(dayjs(field.value))
return (
<Grid item xs={field.size ?? 6} key={field.id}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
@@ -343,8 +345,6 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({
id={field.id}
value={value}
onChange={(event) => {
console.log(event);
console.log(event.target);
onChange(event.target.value);
const newValue = event.target.value;
const selectedOption = field.options?.find(
@@ -379,6 +379,68 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({
</FormControl>
</Grid>
);
} else if (field.type === "multiSelect-Obj") {
return (
<Grid item xs={field.size ?? 6} key={field.id}>
<FormControl fullWidth>
<InputLabel id={`${field.id}-label`}>{field.label}</InputLabel>
<Controller
name={field.id}
control={control}
defaultValue={
field.value !== undefined ? field.value : []
}
render={({ field: { onChange, value } }) => (
<Select
labelId={`${field.id}-label`}
id={field.id}
value={value}
multiple
onChange={(event) => {
onChange(event.target.value);
const newValue = event.target.value;
const selectedOption = field.options?.find(
(option) => option.id === newValue
);
handleAutocompleteChange(
field.id,
selectedOption
);
}}
renderValue={(selected) => {
const selectedOption = field.options?.filter((option) => selected.includes(option.id));
return (
<Stack gap={1} direction="row" flexWrap="wrap">
{selectedOption ? selectedOption.map(({id, label}) => {
return (
<Chip key={id} label={label} />
)}) : null}
</Stack>
)}}
required={field.required}
>
{field.options?.map((option) => (
<MenuItem
value={
option.id !== undefined
? option.id
: option
}
key={
option.id !== undefined
? option.id
: option
}
>
{option.id ? option.label : ""}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
);
} else if (field.type === "numeric") {
return (
<Grid item xs={field.size ?? 6} key={field.id}>


+ 0
- 1
src/components/CustomerDetail/index.ts Bestand weergeven

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

src/components/CustomerDetail/ContactInfo.tsx → src/components/CustomerSave/ContactInfo.tsx Bestand weergeven


src/components/CustomerDetail/CustomerInfo.tsx → src/components/CustomerSave/CustomerInfo.tsx Bestand weergeven


src/components/CustomerDetail/CustomerDetail.tsx → src/components/CustomerSave/CustomerSave.tsx Bestand weergeven

@@ -42,7 +42,7 @@ const hasErrorsInTab = (
}
};

const CustomerDetail: React.FC<Props> = ({
const CustomerSave: React.FC<Props> = ({
subsidiaries,
customerTypes,
}) => {
@@ -199,20 +199,20 @@ const CustomerDetail: React.FC<Props> = ({
setServerError("");

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) {
console.log(e)
setServerError(t("An error has occurred. Please try again later."));
@@ -277,4 +277,4 @@ const CustomerDetail: React.FC<Props> = ({
);
};

export default CustomerDetail;
export default CustomerSave;

src/components/CustomerDetail/CustomerDetailWrapper.tsx → src/components/CustomerSave/CustomerSaveWrapper.tsx Bestand weergeven

@@ -3,7 +3,7 @@
// import { fetchProjectCategories } from "@/app/api/projects";
// import { fetchTeamLeads } from "@/app/api/staff";
import { fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer";
import CustomerDetail from "./CustomerDetail";
import CustomerSave from "./CustomerSave";

// type Props = {
// params: {
@@ -11,7 +11,7 @@ import CustomerDetail from "./CustomerDetail";
// };
// };

const CustomerDetailWrapper: React.FC = async () => {
const CustomerSaveWrapper: React.FC = async () => {
// const { params } = props
// console.log(params)
const [subsidiaries, customerTypes] =
@@ -21,8 +21,8 @@ const CustomerDetailWrapper: React.FC = async () => {
]);

return (
<CustomerDetail subsidiaries={subsidiaries} customerTypes={customerTypes} />
<CustomerSave subsidiaries={subsidiaries} customerTypes={customerTypes} />
);
};

export default CustomerDetailWrapper;
export default CustomerSaveWrapper;

src/components/CustomerDetail/SubsidiaryAllocation.tsx → src/components/CustomerSave/SubsidiaryAllocation.tsx Bestand weergeven


+ 1
- 0
src/components/CustomerSave/index.ts Bestand weergeven

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

+ 22
- 7
src/components/EditStaff/EditStaff.tsx Bestand weergeven

@@ -15,8 +15,15 @@ import { fetchSkillCombo } from "@/app/api/skill/actions";
import { fetchSalaryCombo } from "@/app/api/salarys/actions";
// import { Field } from "react-hook-form";

interface dataType {
[key: string]: any;

interface skill {
id: number;
name: string;
code: string;
}
interface skillObj {
id: number;
skill: skill;
}

interface Options {
@@ -113,7 +120,9 @@ const EditStaff: React.FC = async () => {
if (data) setGradeCombo(data.records);
});
fetchSkillCombo().then((data) => {
if (data) setSkillCombo(data.records);
if (data) {
}setSkillCombo(data.records);
console.log(data.records)
});
fetchSalaryCombo().then((data) => {
if (data) setSalaryCombo(data.records);
@@ -127,6 +136,10 @@ const EditStaff: React.FC = async () => {
console.log(id)
fetchStaffEdit(id).then((staff) => {
console.log(staff.data);
const skillset = staff.data.skillset
console.log(skillset);
const skillIds = skillset.map((item: skillObj) => item.skill.id);
console.log(skillIds)
const data = staff.data;
///////////////////// list 1 /////////////////////
const list1 = keyOrder1
@@ -181,15 +194,17 @@ const EditStaff: React.FC = async () => {
label: t(`Grade`),
type: "combo-Obj",
options: gradeCombo,
value: data[key] !== null ? data[key].id ?? "" : "",
value: data[key]?.id ?? "",
};
case "skill":
console.log(skillIds)
return {
id: `${key}SetId`,
label: t(`Skillset`),
type: "combo-Obj",
type: "multiSelect-Obj",
options: skillCombo,
value: data[key] !== null ? data[key].id ?? "" : "",
value: skillIds ?? [],
//array problem
};
case "currentPosition":
return {
@@ -206,7 +221,7 @@ const EditStaff: React.FC = async () => {
label: t(`Salary Point`),
type: "combo-Obj",
options: salaryCombo,
value: data[key] !== null ? data[key].id ?? "" : "",
value: data[key]?.id ?? "",
required: true,
};
// case "hourlyRate":


+ 13
- 5
src/components/EditTeam/Allocation.tsx Bestand weergeven

@@ -35,9 +35,10 @@ import StarsIcon from "@mui/icons-material/Stars";

export interface Props {
allStaffs: StaffResult[];
teamLead: number;
}

const Allocation: React.FC<Props> = ({ allStaffs: staff }) => {
const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => {
const { t } = useTranslation();
const searchParams = useSearchParams();
const idString = searchParams.get("id");
@@ -53,9 +54,16 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff }) => {

const initialStaffs = staff.map((s) => ({ ...s }));
const [filteredStaff, setFilteredStaff] = useState(initialStaffs);
const [selectedStaff, setSelectedStaff] = useState<typeof filteredStaff>(
filteredStaff.filter((s) => getValues("addStaffIds")?.includes(s.id))
const [selectedStaff, setSelectedStaff] = useState<typeof filteredStaff>(() => {
const rearrangedStaff = filteredStaff.sort((a, b) => {
if (a.id === teamLead) return -1;
if (b.id === teamLead) return 1;
return 0;
});
return rearrangedStaff.filter((s) => getValues("addStaffIds")?.includes(s.id))
}
);
console.log(filteredStaff.filter((s) => getValues("addStaffIds")?.includes(s.id)))
const [seletedTeamLead, setSeletedTeamLead] = useState<number>();
const [deletedStaffIds, setDeletedStaffIds] = useState<number[]>([]);

@@ -84,8 +92,8 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff }) => {
},
getValues("addStaffIds")
);
console.log(rearrangedList);
console.log(selectedStaff);
// console.log(rearrangedList);
// console.log(selectedStaff);

const rearrangedStaff = rearrangedList.map((id) => {
return selectedStaff.find((staff) => staff.id === id);


+ 33
- 6
src/components/EditTeam/EditTeam.tsx Bestand weergeven

@@ -20,12 +20,15 @@ import { StaffResult } from "@/app/api/staff";

interface desc {
id: number;
name: string;
description: string;
teamLead: number;
}

interface Props {
staff: StaffResult[];
desc: desc[];
// teamLead: StaffResult[]
}

const EditTeam: React.FC<Props> = async ({ staff, desc }) => {
@@ -37,6 +40,8 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => {
const [filteredItems, setFilteredItems] = useState<StaffResult[]>();
const [allStaffs, setAllStaffs] = useState<StaffResult[]>();
const [filteredDesc, setFilteredDesc] = useState<string>();
const [filteredName, setFilteredName] = useState<string>();
const [teamLead, setTeamLead] = useState<number>();
const [tabIndex, setTabIndex] = useState(0);
const router = useRouter();
// const [selectedStaff, setSelectedStaff] = useState<typeof filteredItems>(
@@ -63,25 +68,47 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => {
);
useEffect(() => {
let idList: number[] = []
console.log(desc)
if (idString) {
const filteredTeam = staff.filter(
(item) => item.teamId === parseInt(idString)
(item) => {
console.log(item)
console.log(parseInt(idString))
return (item.teamId === parseInt(idString))}
);
console.log(filteredTeam)
const tempDesc = desc.filter(
(item) => item.id === parseInt(idString)
)
// const leader = teamLead.filter(
// (staff) => staff.teamId === parseInt(idString)
// )
// console.log(leader)
console.log(tempDesc[0].teamLead)
setTeamLead(tempDesc[0].teamLead)
if (filteredTeam.length > 0) {
const filteredIds: number[] = filteredTeam.map((i) => (
i.id
))
))
// const teamLead = tempDesc[0].teamLead
// const index = filteredIds.indexOf(teamLead);
// if (index !== -1) {
// filteredIds.splice(index, 1);
// filteredIds.unshift(teamLead);
// }

idList = filteredIds
console.log(filteredIds)
}
// console.log(filteredIds)
console.log(idList)
setFilteredItems(filteredTeam);
formProps.reset({description: tempDesc[0].description, addStaffIds: idList})
setFilteredDesc(tempDesc[0].description)
setFilteredName(tempDesc[0].name)
}
console.log(staff)

setAllStaffs(staff)
@@ -139,7 +166,7 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => {
>

<Typography variant="h4" marginInlineEnd={2}>
{t("Edit Team")}
{t("Edit Team")} - {filteredName}
</Typography>
<Stack
direction="row"
@@ -165,7 +192,7 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => {
</Tabs>
</Stack>
{tabIndex === 0 && <TeamInfo value={filteredDesc!!} />}
{tabIndex === 1 && <Allocation allStaffs={allStaffs!!} />}
{tabIndex === 1 && <Allocation allStaffs={allStaffs!!} teamLead={teamLead!!}/>}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"


+ 21
- 9
src/components/EditTeam/EditTeamWrapper.tsx Bestand weergeven

@@ -4,24 +4,36 @@ import EditTeamLoading from "./EditTeamLoading";
// import { fetchTeam, fetchTeamLeads } from "@/app/api/Team";
import { useSearchParams } from "next/navigation";
import { fetchTeam, fetchTeamDetail } from "@/app/api/team";
import { fetchStaff } from "@/app/api/staff";
import { fetchStaff, fetchStaffWithoutTeam, fetchTeamLeads } from "@/app/api/staff";

interface SubComponents {
Loading: typeof EditTeamLoading;
}

const EditTeamWrapper: React.FC & SubComponents = async () => {
const staff = await fetchStaff()
const allTeams = await fetchTeam();
const teamDesc = allTeams.map((i) => (

const [
staff,
allTeams,
// teamLead,
] = await Promise.all([
fetchStaff(),
fetchTeam(),
// fetchTeamLeads(),
]);
console.log(allTeams)

const teamDesc = allTeams.map((i) => {
return (
{
id: i.id,
description: i.description
name: i.name,
description: i.description,
teamLead: i.teamLead
}
))
console.log(staff)
console.log(teamDesc)
return <EditTeam staff={staff} desc={teamDesc}/>;
)})

return <EditTeam staff={staff} desc={teamDesc} />;
};

EditTeamWrapper.Loading = EditTeamLoading;


+ 49
- 0
src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx Bestand weergeven

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

import React, { useMemo } from "react";
import SearchBox, { Criterion } from "../SearchBox";
import { useTranslation } from "react-i18next";
import { ProjectResult } from "@/app/api/projects";
import { EX02ProjectCashFlowReportFilter } from "@/app/api/reports";
import { fetchEX02ProjectCashFlowReport } from "@/app/api/reports/actions";
import { downloadFile } from "@/app/utils/commonUtil";
import { BASE_API_URL } from "@/config/api";

interface Props {
projects: ProjectResult[];
}

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

const GenerateEX02ProjectCashFlowReport: React.FC<Props> = ({ projects }) => {
const { t } = useTranslation();
const projectCombo = projects.map(project => `${project.code} - ${project.name}`)

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: t("Project"), paramName: "project", type: "select", options: projectCombo, needAll: false},
],
[t],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={async (query) => {

if (query.project.length > 0 && query.project.toLocaleLowerCase() !== "all") {
const projectIndex = projectCombo.findIndex(project => project === query.project)
const response = await fetchEX02ProjectCashFlowReport({ projectId: projects[projectIndex].id })
if (response) {
downloadFile(new Uint8Array(response.blobValue), response.filename!!)
}
}
}}
/>
</>
);
};

export default GenerateEX02ProjectCashFlowReport;

+ 38
- 0
src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx Bestand weergeven

@@ -0,0 +1,38 @@
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 GenerateEX02ProjectCashFlowReportLoading: React.FC = () => {
return (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<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 GenerateEX02ProjectCashFlowReportLoading;

+ 18
- 0
src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx Bestand weergeven

@@ -0,0 +1,18 @@
import React from "react";
import GenerateEX02ProjectCashFlowReportLoading from "./GenerateEX02ProjectCashFlowReportLoading";
import { fetchProjects } from "@/app/api/projects";
import GenerateEX02ProjectCashFlowReport from "./GenerateEX02ProjectCashFlowReport";

interface SubComponents {
Loading: typeof GenerateEX02ProjectCashFlowReportLoading;
}

const GenerateEX02ProjectCashFlowReportWrapper: React.FC & SubComponents = async () => {
const projects = await fetchProjects();

return <GenerateEX02ProjectCashFlowReport projects={projects} />;
};

GenerateEX02ProjectCashFlowReportWrapper.Loading = GenerateEX02ProjectCashFlowReportLoading;

export default GenerateEX02ProjectCashFlowReportWrapper;

+ 1
- 0
src/components/GenerateEX02ProjectCashFlowReport/index.ts Bestand weergeven

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

+ 14
- 0
src/components/NavigationContent/NavigationContent.tsx Bestand weergeven

@@ -32,6 +32,9 @@ import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig";
import Logo from "../Logo";
import GroupIcon from '@mui/icons-material/Group';
import BusinessIcon from '@mui/icons-material/Business';
import ViewWeekIcon from '@mui/icons-material/ViewWeek';
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';

interface NavigationItem {
icon: React.ReactNode;
@@ -77,6 +80,11 @@ const navigationItems: NavigationItem[] = [
label: "Staff Utilization",
path: "/dashboard/StaffUtilization",
},
{
icon: <ViewWeekIcon />,
label: "Project Resource Summary",
path: "/dashboard/ProjectResourceSummary",
}
],
},
{
@@ -107,6 +115,10 @@ const navigationItems: NavigationItem[] = [
{icon: <Analytics />, label:"Cost and Expense Report", path: "/analytics/CostandExpenseReport"},
{icon: <Analytics />, label:"Completion Report", path: "/analytics/ProjectCompletionReport"},
{icon: <Analytics />, label:"Completion Report with Outstanding Un-billed Hours Report", path: "/analytics/ProjectCompletionReportWO"},
{icon: <Analytics />, label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"},
{icon: <Analytics />, label:"Project P&L Report", path: "/analytics/ProjectPLReport"},
{icon: <Analytics />, label:"Financial Status Report", path: "/analytics/FinancialStatusReport"},
{icon: <Analytics />, label:"EX02 - Project Cash Flow Report", path: "/analytics/EX02ProjectCashFlowReport"},
],
},
{
@@ -116,10 +128,12 @@ const navigationItems: NavigationItem[] = [
{ icon: <BusinessIcon />, label: "Subsidiary", path: "/settings/subsidiary" },
{ icon: <Staff />, label: "Staff", path: "/settings/staff" },
{ icon: <Company />, label: "Company", path: "/settings/company" },
{ icon: <EmojiEventsIcon />, label: "Skill", path: "/settings/skill" },
{ icon: <Department />, label: "Department", path: "/settings/department" },
{ icon: <Position />, label: "Position", path: "/settings/position" },
{ icon: <Salary />, label: "Salary", path: "/settings/salary" },
{ icon: <Team />, label: "Team", path: "/settings/team" },
{ icon: <ManageAccountsIcon />, label: "User", path: "/settings/user" },
{ icon: <Holiday />, label: "Holiday", path: "/settings/holiday" },
],
},


+ 2
- 1
src/components/ProgressByClient/ProgressByClient.tsx Bestand weergeven

@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
import { Card, CardHeader } from "@mui/material";
import CustomSearchForm from "../CustomSearchForm/CustomSearchForm";
import CustomDatagrid from "../CustomDatagrid/CustomDatagrid";
import ReactApexChart from "react-apexcharts";
// import ReactApexChart from "react-apexcharts";
import { ApexOptions } from "apexcharts";
import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
import ReportProblemIcon from "@mui/icons-material/ReportProblem";
@@ -18,6 +18,7 @@ import { AnyARecord, AnyCnameRecord } from "dns";
import SearchBox, { Criterion } from "../SearchBox";
import ProgressByClientSearch from "@/components/ProgressByClientSearch";
import { Suspense } from "react";
const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false });

const ProgressByClient: React.FC = () => {
const [activeTab, setActiveTab] = useState("financialSummary");


+ 18
- 3
src/components/ProgressByClientSearch/ProgressByClientSearch.tsx Bestand weergeven

@@ -1,11 +1,13 @@
"use client";

import { ProjectResult } from "@/app/api/projects";
import React, { useMemo, useState } from "react";
import React, { useMemo, useState, useCallback } from "react";
import SearchBox, { Criterion } from "../SearchBox";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import { ClientProjectResult } from "@/app/api/clientprojects";
import EditNote from "@mui/icons-material/EditNote";
import { useRouter, useSearchParams } from "next/navigation";

interface Props {
projects: ClientProjectResult[];
@@ -15,7 +17,7 @@ type SearchParamNames = keyof SearchQuery;

const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {
const { t } = useTranslation("projects");
const searchParams = useSearchParams()
// If project searching is done on the server-side, then no need for this.
const [filteredProjects, setFilteredProjects] = useState(projects);

@@ -27,15 +29,28 @@ const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {
[t],
);

const onTaskClick = useCallback((clientProjectResult: ClientProjectResult) => {
const params = new URLSearchParams(searchParams.toString())
params.set("id", clientProjectResult.id.toString())
console.log(clientProjectResult)
}, []);

const columns = useMemo<Column<ClientProjectResult>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: onTaskClick,
buttonIcon: <EditNote />,
},
{ name: "clientCode", label: t("Client Code") },
{ name: "clientName", label: t("Client Name") },
{ name: "SubsidiaryClientCode", label: t("Subsidiary Code") },
{ name: "SubsidiaryClientName", label: t("Subisdiary") },
{ name: "NoOfProjects", label: t("No. of Projects") },
],
[t],
[onTaskClick, t],
// [t],
);

return (


+ 2
- 1
src/components/ProgressByTeam/ProgressByTeam.tsx Bestand weergeven

@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
import { Card, CardHeader } from "@mui/material";
import CustomSearchForm from "../CustomSearchForm/CustomSearchForm";
import CustomDatagrid from "../CustomDatagrid/CustomDatagrid";
import ReactApexChart from "react-apexcharts";
// import ReactApexChart from "react-apexcharts";
import { ApexOptions } from "apexcharts";
import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
import ReportProblemIcon from "@mui/icons-material/ReportProblem";
@@ -18,6 +18,7 @@ import { AnyARecord, AnyCnameRecord } from "dns";
import SearchBox, { Criterion } from "../SearchBox";
import ProgressByTeamSearch from "@/components/ProgressByTeamSearch";
import { Suspense } from "react";
const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false });

const ProgressByTeam: React.FC = () => {
const [activeTab, setActiveTab] = useState("financialSummary");


+ 548
- 0
src/components/ProjectResourceSummary/ProjectResourceSummary.tsx Bestand weergeven

@@ -0,0 +1,548 @@
"use client";
import * as React from "react";
import Grid from "@mui/material/Grid";
import { useState, useEffect, useMemo } from "react";
import Paper from "@mui/material/Paper";
import { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import { Card, CardHeader } from "@mui/material";
import CustomSearchForm from "../CustomSearchForm/CustomSearchForm";
import CustomDatagrid from "../CustomDatagrid/CustomDatagrid";
import ReactApexChart from "react-apexcharts";
import { ApexOptions } from "apexcharts";
import { DataGrid, GridColDef, GridRowSelectionModel} from "@mui/x-data-grid";
import ReportProblemIcon from "@mui/icons-material/ReportProblem";
import dynamic from "next/dynamic";
import "../../app/global.css";
import { AnyARecord, AnyCnameRecord } from "dns";
import SearchBox, { Criterion } from "../SearchBox";
import ProgressByClientSearch from "@/components/ProgressByClientSearch";
import { Suspense } from "react";
import { getPossibleInstrumentationHookFilenames } from "next/dist/build/utils";
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Collapse from '@mui/material/Collapse';
import IconButton from '@mui/material/IconButton';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';


const ProjectResourceSummary: React.FC = () => {
const [SearchCriteria, setSearchCriteria] = React.useState({});
const { t } = useTranslation("dashboard");
const [selectionModel, setSelectionModel]: any[] = React.useState([]);
const [projectName, setProjectName]:any = React.useState("NA");
const [projectFee, setProjectFee]:any = React.useState(0);
const [status, setStatus]:any = React.useState("NA");
const [plannedResources, setPlannedResources]:any = React.useState(0);
const [actualResourcesSpent, setActualResourcesSpent]:any = React.useState(0);
const [remainingResources, setRemainingResources]:any = React.useState(0);

function createData(stage:any, taskCount:any, g1Planned:any, g1Actual:any, g2Planned:any, g2Actual:any, g3Planned:any, g3Actual:any, g4Planned:any, g4Actual:any, g5Planned:any, g5Actual:any, totalPlanned:any, totalActual:any, task:any) {
return {
stage,
taskCount,
g1Planned,
g1Actual,
g2Planned,
g2Actual,
g3Planned,
g3Actual,
g4Planned,
g4Actual,
g5Planned,
g5Actual,
totalPlanned,
totalActual,
task:task
}
}

function createTaskData(stage:any, taskCount:any, g1Planned:any, g1Actual:any, g2Planned:any, g2Actual:any, g3Planned:any, g3Actual:any, g4Planned:any, g4Actual:any, g5Planned:any, g5Actual:any, totalPlanned:any, totalActual:any) {
return {
stage,
taskCount,
g1Planned,
g1Actual,
g2Planned,
g2Actual,
g3Planned,
g3Actual,
g4Planned,
g4Actual,
g5Planned,
g5Actual,
totalPlanned,
totalActual,
}
}

const task1Rows:any = [
{stage:"1.1 Preparation of preliminary...",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"},
{stage:"1.2 Cash flow forecast",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"},
{stage:"1.3 Cost studies for alterative design solutions",taskCount:"-",g1Planned:"-",g1Actual:"115.00",g2Planned:"-", g2Actual:"36.00", g3Planned:"-", g3Actual:"28.00", g4Planned: "-", g4Actual:"7.00", g5Planned:"-", g5Actual:"1.75", totalPlanned:"-", totalActual:"188.00"},
{stage:"1.4 Attend design co-ordiantion / project",taskCount:"-",g1Planned:"-",g1Actual:"29.00",g2Planned:"-", g2Actual:"9.00", g3Planned:"-", g3Actual:"7.00", g4Planned: "-", g4Actual:"2.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"48.00"},
{stage:"1.5 Prepare / Review RIC",taskCount:"-",g1Planned:"-",g1Actual:"88.00",g2Planned:"-", g2Actual:"27.00", g3Planned:"-", g3Actual:"21.00", g4Planned: "-", g4Actual:"5.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"141.75"}
]

const task2Rows:any = [
]

const task3Rows:any = [
]

const task4Rows:any = [
]

const task5Rows:any = [
]

const task6Rows:any = [
]

const rows = [
createData("Stage 1 - Design & Cost Planning / Estimating","5","576.00","576.00","192.00", "180.00", "144.00", "140.00", "38.40", "38.00", "9.60", "9.75", "960.00", "943.75",task1Rows),
createData("Stage 2 - Tender Documentation","11", "384.00", "382.00", "128.00", "130.00", "96.00", "79.00", "25.60", "25.00", "6.40", "4.00", "640.00", "620.00",task2Rows),
createData("Stage 3 - Tender Analysis & Report & Contract Documentation","7", "384.00", "300.00", "128.00", "130.00", "96.00", "79.00", "25.60", "25.00", "6.40", "4.00", "640.00", "538.00",task3Rows),
createData("Stage 4 - Construction", "13", "480.00", "400.00", "160.00", "160.00", "120.00", "128.00", "32.00", "25.00", "8.00", "3.00", "800.00", "716.00",task4Rows),
createData("Stage 5 - Miscellaneous", "4", "96.00", "-", "32.00", "-", "24.00", "-0", "6.40", "-", "1.600", "-", "160.00", "-",task5Rows),
createData("","Total", "1920.00", "1658.00", "640.00", "600.00", "480.00", "426.00", "128.00", "113.00", "32.00", "20.75", "3,200.00", "2817.75",task6Rows),
];

// const taskRows = [
// createTaskData("1.1 Preparation of preliminary...","-","-","172.00","-","54.00","-","42.00","-","12.00","-","3.00","-","283.00"),
// ];

function Row(props:any) {
const { row } = props;
const [open, setOpen] = React.useState(false);
return (
<React.Fragment>
<TableRow sx={{ '& > *': { borderBottom: 'unset' } }}>
<TableCell>
{row.task.length > 0 && (
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
)}
</TableCell>
<TableCell style={{fontSize:10}}>{row.stage}</TableCell>
<TableCell style={{fontSize:10}}>{row.taskCount}</TableCell>
<TableCell style={{fontSize:10}}>{row.g1Planned}</TableCell>
<TableCell style={{fontSize:10}}>{row.g1Actual}</TableCell>
<TableCell style={{fontSize:10}}>{row.g2Planned}</TableCell>
<TableCell style={{fontSize:10}}>{row.g2Actual}</TableCell>
<TableCell style={{fontSize:10}}>{row.g3Planned}</TableCell>
<TableCell style={{fontSize:10}}>{row.g3Actual}</TableCell>
<TableCell style={{fontSize:10}}>{row.g4Planned}</TableCell>
<TableCell style={{fontSize:10}}>{row.g4Actual}</TableCell>
<TableCell style={{fontSize:10}}>{row.g5Planned}</TableCell>
<TableCell style={{fontSize:10}}>{row.g5Actual}</TableCell>
<TableCell style={{fontSize:10}}>{row.totalPlanned}</TableCell>
<TableCell style={{fontSize:10}}>{row.totalActual}</TableCell>
</TableRow>
{row.task.map((taskRow:any) => (
<>
<TableRow style={{backgroundColor:"#f0f3f7"}}>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell colSpan={1}/>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.stage}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.taskCount}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g1Planned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g1Actual}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g2Planned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g2Actual}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g3Planned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g3Actual}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g4Planned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g4Actual}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g5Planned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g5Actual}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.totalPlanned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.totalActual}</TableCell>
</Collapse>
</TableCell>
</TableRow>
</>
))}
{/* <TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0, borderStyle:"dotted"}} colSpan={15}>
<Collapse in={open} timeout="auto" unmountOnExit style={{borderStyle:"dotted", marginLeft:-17, marginRight:-17}}>
<Box sx={{ margin: 1 }}>
{row.task.map((taskRow:any) => (
<TableRow key={taskRow.stage}>
<TableCell style={{borderStyle:"dotted"}}/>
<TableCell style={{fontSize:10, borderStyle:"dotted"}}>{taskRow.stage}</TableCell>
<TableCell style={{fontSize:10, borderStyle:"dotted"}}>{taskRow.taskCount}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g1Planned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g1Actual}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g2Planned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g2Actual}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g3Planned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g3Actual}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g4Planned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g4Actual}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g5Planned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g5Actual}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.totalPlanned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.totalActual}</TableCell>
</TableRow>
))}
</Box>
</Collapse>
</TableCell>
</TableRow> */}
</React.Fragment>
);
}

useEffect(() => {
setProjectName("C-1001-001 - Consultancy Project A")
const fee = 2000000
setProjectFee(fee.toLocaleString())
setStatus("Within Budget / Overconsumption")
const plannedResourcesInt = 3200
setPlannedResources(plannedResourcesInt.toLocaleString())
const actualResourcesSpentInt = 2817.75
setActualResourcesSpent(actualResourcesSpentInt.toLocaleString())
const remainingResourcesInt = 382.25
setRemainingResources(remainingResourcesInt.toLocaleString())
}, [])

const projectResourcesRows = [
{id: 1,stage:"Stage 1 - Design & Cost Planning / Estimating",taskCount:"5",g1Planned:"576.00",g1Actual:"576.00",g2Planned:"192.00", g2Actual:"180.00", g3Planned:"144.00", g3Actual:"140.00", g4Planned: "38.40", g4Actual:"38S.00", g5Planned:"9.60", g5Actual:"9.75", totalPlanned:"960.00", totalActual:"943.75"},
{id: 2,stage:"1.1 Preparation of preliminary...",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"},
{id: 3,stage:"1.2 Cash flow forecast",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"},
{id: 4,stage:"1.3 Cost studies for alterative design solutions",taskCount:"-",g1Planned:"-",g1Actual:"115.00",g2Planned:"-", g2Actual:"36.00", g3Planned:"-", g3Actual:"28.00", g4Planned: "-", g4Actual:"7.00", g5Planned:"-", g5Actual:"1.75", totalPlanned:"-", totalActual:"188.00"},
{id: 5,stage:"1.4 Attend design co-ordiantion / project",taskCount:"-",g1Planned:"-",g1Actual:"29.00",g2Planned:"-", g2Actual:"9.00", g3Planned:"-", g3Actual:"7.00", g4Planned: "-", g4Actual:"2.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"48.00"},
{id: 6,stage:"1.5 Prepare / Review RIC",taskCount:"-",g1Planned:"-",g1Actual:"88.00",g2Planned:"-", g2Actual:"27.00", g3Planned:"-", g3Actual:"21.00", g4Planned: "-", g4Actual:"5.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"141.75"},
{id: 7,stage:"Stage 2 - Tender Documentation",taskCount:"11",g1Planned:"384.00",g1Actual:"382.00",g2Planned:"128.00", g2Actual:"130.00", g3Planned:"96.00", g3Actual:"79.00", g4Planned: "25.60", g4Actual:"25.00", g5Planned:"6.40", g5Actual:"4.00", totalPlanned:"640.00", totalActual:"620.00"},
{id: 8,stage:"Stage 3 - Tender Analysis & Report & Contract Documentation",taskCount:"7",g1Planned:"384.00",g1Actual:"300.00",g2Planned:"128.00", g2Actual:"130.00", g3Planned:"96.00", g3Actual:"79.00", g4Planned: "25.60", g4Actual:"25.00", g5Planned:"6.40", g5Actual:"4.00", totalPlanned:"640.00", totalActual:"538.00"},
{id: 9,stage:"Stage 4 - Construction",taskCount:"13",g1Planned:"480.00",g1Actual:"400.00",g2Planned:"160.00", g2Actual:"160.00", g3Planned:"120.00", g3Actual:"128.00", g4Planned: "32.00", g4Actual:"25.00", g5Planned:"8.00", g5Actual:"3.00", totalPlanned:"800.00", totalActual:"716.00"},
{id: 10,stage:"Stage 5 - Miscellaneous",taskCount:"4",g1Planned:"96.00",g1Actual:"-",g2Planned:"32.00", g2Actual:"-", g3Planned:"24.00", g3Actual:"-0", g4Planned: "6.40", g4Actual:"-", g5Planned:"1.600", g5Actual:"-", totalPlanned:"160.00", totalActual:"-"},
{id: 11,stage:"",taskCount:"Total",g1Planned:"1920.00",g1Actual:"1658.00",g2Planned:"640.00", g2Actual:"600.00", g3Planned:"480.00", g3Actual:"426.00", g4Planned: "128.00", g4Actual:"113.00", g5Planned:"32.00", g5Actual:"20.75", totalPlanned:"3,200.00", totalActual:"2817.75"},
]

const columns2 = [
{
id: 'stage',
field: 'stage',
headerName: "Stage",
flex: 2,
},
{
id: 'taskCount',
field: 'taskCount',
headerName: "Task Count",
flex: 0.5,
},
{
id: 'g1Planned',
field: 'g1Planned',
headerName: "Planned",
flex: 0.7,
},
{
id: 'g1Actual',
field: 'g1Actual',
headerName: "Actual",
flex: 0.7,
},
{
id: 'g2Planned',
field: 'g2Planned',
headerName: "Planned",
flex: 0.7,
},
{
id: 'g2Actual',
field: 'g2Actual',
headerName: "Actual",
flex: 0.7,
},
{
id: 'g3Planned',
field: 'g3Planned',
headerName: "Planned",
flex: 0.7,
},
{
id: 'g3Actual',
field: 'g3Actual',
headerName: "Actual",
flex: 0.7,
},
{
id: 'g4Planned',
field: 'g4Planned',
headerName: "Planned",
flex: 0.7,
},
{
id: 'g4Actual',
field: 'g4Actual',
headerName: "Actual",
flex: 0.7,
},
{
id: 'g5Planned',
field: 'g5Planned',
headerName: "Planned",
flex: 0.7,
},
{
id: 'g5Actual',
field: 'g5Actual',
headerName: "Actual",
flex: 0.7,
},
{
id: 'totalPlanned',
field: 'totalPlanned',
headerName: "Planned",
flex: 0.7,
},
{
id: 'totalActual',
field: 'totalActual',
headerName: "Actual",
flex: 0.7,
},
];

const columnGroupingModel = [
{
groupId: 'G1',
children: [{ field: 'g1Planned' },{ field: 'g1Actual' }],
headerClassName: 'groupColor',
},
{
groupId: 'G2',
children: [{ field: 'g2Planned' },{ field: 'g2Actual' }],
headerClassName: 'groupColor',
},
{
groupId: 'G3',
children: [{ field: 'g3Planned' },{ field: 'g3Actual' }],
headerClassName: 'groupColor',
},
{
groupId: 'G4',
children: [{ field: 'g4Planned' },{ field: 'g4Actual' }],
headerClassName: 'groupColor',
},
{
groupId: 'G5',
children: [{ field: 'g5Planned' },{ field: 'g5Actual' }],
headerClassName: 'groupColor',
},
{
groupId: 'Total',
children: [{ field: 'totalPlanned' },{ field: 'totalActual' }],
headerClassName: 'totalGroupColor',
},
];

return (
<Grid item sm sx={{
'& .groupColor': {
backgroundColor: 'rgba(240, 240, 240, 0.55)',
},
'& .totalGroupColor': {
backgroundColor: 'rgba(218, 218, 245, 0.55)',
},
}}>
<Card className="mt-5">
<CardHeader className="text-slate-500" title="Project Information"/>
<div className="ml-6 mr-6">
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{fontSize:"1em", fontWeight:"bold"}}>
<u>
Project
</u>
</div>
<div style={{fontSize:"1em"}}>
{projectName}
</div>
</div>
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{ fontSize:"1em", fontWeight:"bold"}}>
<u>
Project Fee
</u>
</div>
<div style={{fontSize:"1em"}}>
HKD {projectFee}
</div>
</div>
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{ fontSize:"1em", fontWeight:"bold"}}>
<u>
Status
</u>
</div>
<div style={{fontSize:"1em"}}>
{status}
</div>
</div>
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{ fontSize:"1em", fontWeight:"bold"}}>
<u>
Planned Resources
</u>
</div>
<div style={{fontSize:"1em"}}>
{plannedResources} Manhours
</div>
</div>
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{ fontSize:"1em", fontWeight:"bold"}}>
<u>
Actual Resources Spent
</u>
</div>
<div style={{fontSize:"1em"}}>
{actualResourcesSpent} Manhours
</div>
</div>
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{ fontSize:"1em", fontWeight:"bold"}}>
<u>
Remaining Resources
</u>
</div>
<div style={{fontSize:"1em"}}>
{remainingResources} Manhours
</div>
</div>
</div>
{/* <div style={{display:"inline-block",width:"99%",marginLeft:10}}>
<CustomDatagrid rows={projectResourcesRows} columns={columns2} columnWidth={200} dataGridHeight={480} pageSize={100} columnGroupingModel={columnGroupingModel} sx={{fontSize:10}}/>
</div> */}
<div style={{display:"inline-block",width:"99%",marginLeft:10, marginRight:10, marginTop:10}}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="center" colSpan={3}>
</TableCell>
<TableCell align="center" colSpan={2}>
G1
</TableCell>
<TableCell align="center" colSpan={2}>
G2
</TableCell>
<TableCell align="center" colSpan={2}>
G3
</TableCell>
<TableCell align="center" colSpan={2}>
G4
</TableCell>
<TableCell align="center" colSpan={2}>
G5
</TableCell>
<TableCell align="center" colSpan={2} style={{backgroundColor:"rgba(218, 218, 245, 0.55)"}}>
Total
</TableCell>
</TableRow>
<TableRow>
<TableCell style={{width:"5%"}}/>
<TableCell style={{fontSize:10, width:"20%"}}>Stage</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Task Count</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<Row key={row.stage} row={row} />
))}
</TableBody>
</Table>
</TableContainer>
</div>
</Card>
</Grid>
);
};

export default ProjectResourceSummary;

+ 1
- 0
src/components/ProjectResourceSummary/index.ts Bestand weergeven

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

+ 75
- 0
src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx Bestand weergeven

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

import { ProjectResult } from "@/app/api/projects";
import React, { useMemo, useState, useCallback } from "react";
import SearchBox, { Criterion } from "../SearchBox";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import { ResourceSummaryResult } from "@/app/api/resourcesummary";
import EditNote from "@mui/icons-material/EditNote";
import { useRouter, useSearchParams } from "next/navigation";
import ProjectResourceSummary from "@/components/ProjectResourceSummary";
import ArticleIcon from '@mui/icons-material/Article';

interface Props {
projects: ResourceSummaryResult[];
}
type SearchQuery = Partial<Omit<ResourceSummaryResult, "id">>;
type SearchParamNames = keyof SearchQuery;


const ProjectResourceSummarySearch: React.FC<Props> = ({ projects }) => {
const { t } = useTranslation("projects");
const searchParams = useSearchParams()
// If project searching is done on the server-side, then no need for this.
const [filteredProjects, setFilteredProjects] = useState(projects);

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: "Project Code", paramName: "projectCode", type: "text" },
{ label: "Project Name", paramName: "projectName", type: "text" },
{ label: "Client Code", paramName: "clientCode", type: "text" },
{ label: "Client Name", paramName: "clientName", type: "text" },
],
[t],
);

const onTaskClick = useCallback((resourceSummaryResult: ResourceSummaryResult) => {
console.log(resourceSummaryResult)
}, []);


const columns = useMemo<Column<ResourceSummaryResult>[]>(
() => [
{
name: "id",
label: t("View"),
onClick: onTaskClick,
buttonIcon: <ArticleIcon />,
},
{ name: "projectCode", label: t("Project Code") },
{ name: "projectName", label: t("Project Name") },
{ name: "clientCodeAndName", label: t("Client Code And Name") },
],
[onTaskClick, t],
// [t],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
console.log(query);
}}
/>
<SearchResults<ResourceSummaryResult>
items={filteredProjects}
columns={columns}
/>
<ProjectResourceSummary/>
</>
);
};

export default ProjectResourceSummarySearch;

+ 40
- 0
src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx Bestand weergeven

@@ -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 ProjectResourceSummarySearchLoading: 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 ProjectResourceSummarySearchLoading;

+ 20
- 0
src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx Bestand weergeven

@@ -0,0 +1,20 @@
import { fetchResourceSummary } from "@/app/api/resourcesummary";
import React from "react";
import ProjectResourceSummarySearch from "./ProjectResourceSummarySearch";
import ProjectResourceSummarySearchLoading from "./ProjectResourceSummarySearchLoading";

interface SubComponents {
Loading: typeof ProjectResourceSummarySearchLoading;
}

const ProjectResourceSummarySearchWrapper: React.FC & SubComponents = async () => {
const clentprojects = await fetchResourceSummary();

return <ProjectResourceSummarySearch projects={clentprojects} />;
};

ProjectResourceSummarySearchWrapper.Loading = ProjectResourceSummarySearchLoading;

export default ProjectResourceSummarySearchWrapper;



+ 1
- 0
src/components/ProjectResourceSummarySearch/index.ts Bestand weergeven

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

+ 8
- 3
src/components/ProjectSearch/ProjectSearch.tsx Bestand weergeven

@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import uniq from "lodash/uniq";
import { useRouter } from "next/navigation";

interface Props {
projects: ProjectResult[];
@@ -17,6 +18,7 @@ type SearchQuery = Partial<Omit<ProjectResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
const router = useRouter();
const { t } = useTranslation("projects");

const [filteredProjects, setFilteredProjects] = useState(projects);
@@ -51,9 +53,12 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
setFilteredProjects(projects);
}, [projects]);

const onProjectClick = useCallback((project: ProjectResult) => {
console.log(project);
}, []);
const onProjectClick = useCallback(
(project: ProjectResult) => {
router.push(`/projects/edit?id=${project.id}`);
},
[router],
);

const columns = useMemo<Column<ProjectResult>[]>(
() => [


+ 17
- 0
src/components/Report/FinancialStatusReport/FinancialStatusReport.tsx Bestand weergeven

@@ -0,0 +1,17 @@
//src\components\DelayReport\DelayReport.tsx
"use client";
import * as React from "react";
import "../../../app/global.css";
import { Suspense } from "react";
import FinancialStatusReportGen from "@/components/Report/FinancialStatusReportGen";

const FinancialStatusReport: React.FC = () => {

return (
<Suspense fallback={<FinancialStatusReportGen.Loading />}>
<FinancialStatusReportGen />
</Suspense>
);
};

export default FinancialStatusReport;

+ 2
- 0
src/components/Report/FinancialStatusReport/index.ts Bestand weergeven

@@ -0,0 +1,2 @@
//src\components\LateStartReport\index.ts
export { default } from "./FinancialStatusReport";

+ 43
- 0
src/components/Report/FinancialStatusReportGen/FinancialStatusReportGen.tsx Bestand weergeven

@@ -0,0 +1,43 @@
//src\components\LateStartReportGen\LateStartReportGen.tsx
"use client";
import React, { useMemo, useState } from "react";
import SearchBox, { Criterion } from "../ReportSearchBoxe1";
import { useTranslation } from "react-i18next";
import { FinancialStatus } from "@/app/api/reporte1";
//import { DownloadReportButton } from './DownloadReportButton';
interface Props {
projects: FinancialStatus[];
}
type SearchQuery = Partial<Omit<FinancialStatus, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {
const { t } = useTranslation("projects");

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: "{Project Code}", paramName: "projectCode", type: "select", options: ["M1234", "M1268", "M1352", "M1393"] },
// {
// label: "Status",
// label2: "Remained Date To",
// paramName: "targetEndDate",
// type: "dateRange",
// },
],
[t],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
console.log(query);
}}
/>
{/* <DownloadReportButton /> */}
</>
);
};

export default ProgressByClientSearch;

+ 41
- 0
src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenLoading.tsx Bestand weergeven

@@ -0,0 +1,41 @@
//src\components\LateStartReportGen\LateStartReportGenLoading.tsx
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 FinancialStatusReportGenLoading: 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 FinancialStatusReportGenLoading;

+ 19
- 0
src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenWrapper.tsx Bestand weergeven

@@ -0,0 +1,19 @@
//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx
import { fetchProjectsFinancialStatus } from "@/app/api/reporte1";
import React from "react";
import FinancialStatusReportGen from "./FinancialStatusReportGen";
import FinancialStatusReportGenLoading from "./FinancialStatusReportGenLoading";

interface SubComponents {
Loading: typeof FinancialStatusReportGenLoading;
}

const FinancialStatusReportGenWrapper: React.FC & SubComponents = async () => {
const clentprojects = await fetchProjectsFinancialStatus();

return <FinancialStatusReportGen projects={clentprojects} />;
};

FinancialStatusReportGenWrapper.Loading = FinancialStatusReportGenLoading;

export default FinancialStatusReportGenWrapper;

+ 2
- 0
src/components/Report/FinancialStatusReportGen/index.ts Bestand weergeven

@@ -0,0 +1,2 @@
//src\components\DelayReportGen\index.ts
export { default } from "./FinancialStatusReportGenWrapper";

+ 17
- 0
src/components/Report/ProjectClaimsReport/ProjectClaimsReport.tsx Bestand weergeven

@@ -0,0 +1,17 @@
//src\components\LateStartReport\LateStartReport.tsx
"use client";
import * as React from "react";
import "../../../app/global.css";
import { Suspense } from "react";
import ProjectClaimsReportGen from "@/components/Report/ProjectClaimsReportGen";

const ProjectClaimsReport: React.FC = () => {

return (
<Suspense fallback={<ProjectClaimsReportGen.Loading />}>
<ProjectClaimsReportGen />
</Suspense>
);
};

export default ProjectClaimsReport;

+ 2
- 0
src/components/Report/ProjectClaimsReport/index.ts Bestand weergeven

@@ -0,0 +1,2 @@
//src\components\LateStartReport\index.ts
export { default } from "./ProjectClaimsReport";

+ 44
- 0
src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGen.tsx Bestand weergeven

@@ -0,0 +1,44 @@
//src\components\LateStartReportGen\LateStartReportGen.tsx
"use client";
import React, { useMemo, useState } from "react";
import SearchBox, { Criterion } from "../ReportSearchBox7";
import { useTranslation } from "react-i18next";
import { ProjectClaims } from "@/app/api/report7";
//import { DownloadReportButton } from './DownloadReportButton';
interface Props {
projects: ProjectClaims[];
}
type SearchQuery = Partial<Omit<ProjectClaims, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {
const { t } = useTranslation("projects");

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{
label: "Report Period From",
label2: "Report Period To",
paramName: "targetEndDate",
type: "dateRange",
},
{ label: "Project Code", paramName: "projectCode", type: "select", options: ["M1963", "M1235", "M1476"] },
{ label: "Staff Name", paramName: "staffName", type: "select", options: ["Kennith", "Tom", "Cyril"] },
],
[t],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
console.log(query);
}}
/>
{/* <DownloadReportButton /> */}
</>
);
};

export default ProgressByClientSearch;

+ 41
- 0
src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenLoading.tsx Bestand weergeven

@@ -0,0 +1,41 @@
//src\components\LateStartReportGen\LateStartReportGenLoading.tsx
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 ProjectClaimsReportGenLoading: 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 ProjectClaimsReportGenLoading;

+ 19
- 0
src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenWrapper.tsx Bestand weergeven

@@ -0,0 +1,19 @@
//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx
import { fetchProjectsProjectClaims } from "@/app/api/report7";
import React from "react";
import ProjectClaimsReportGen from "./ProjectClaimsReportGen";
import ProjectClaimsReportGenLoading from "./ProjectClaimsReportGenLoading";

interface SubComponents {
Loading: typeof ProjectClaimsReportGenLoading;
}

const ProjectClaimsReportGenWrapper: React.FC & SubComponents = async () => {
const clentprojects = await fetchProjectsProjectClaims();

return <ProjectClaimsReportGen projects={clentprojects} />;
};

ProjectClaimsReportGenWrapper.Loading = ProjectClaimsReportGenLoading;

export default ProjectClaimsReportGenWrapper;

Some files were not shown because too many files changed in this diff

Laden…
Annuleren
Opslaan