diff --git a/package-lock.json b/package-lock.json index 84bf9b7..ed756cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/public/temp/AR04_Cost and Expense Report.xlsx b/public/temp/AR04_Cost and Expense Report.xlsx index a414e1a..3ce7f59 100644 Binary files a/public/temp/AR04_Cost and Expense Report.xlsx and b/public/temp/AR04_Cost and Expense Report.xlsx differ diff --git a/public/temp/AR05_Project Completion Report.xlsx b/public/temp/AR05_Project Completion Report.xlsx index 60deae8..152541a 100644 Binary files a/public/temp/AR05_Project Completion Report.xlsx and b/public/temp/AR05_Project Completion Report.xlsx differ diff --git a/public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx b/public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx index 419bd2e..e6f1f35 100644 Binary files a/public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx and b/public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx differ diff --git a/public/temp/AR07_Project Claims Report.xlsx b/public/temp/AR07_Project Claims Report.xlsx new file mode 100644 index 0000000..95b2c13 Binary files /dev/null and b/public/temp/AR07_Project Claims Report.xlsx differ diff --git a/public/temp/AR08_Project P&L Report.xlsx b/public/temp/AR08_Project P&L Report.xlsx new file mode 100644 index 0000000..de45dbe Binary files /dev/null and b/public/temp/AR08_Project P&L Report.xlsx differ diff --git a/public/temp/EX01_Financial Status Report.xlsx b/public/temp/EX01_Financial Status Report.xlsx new file mode 100644 index 0000000..bd1b55d Binary files /dev/null and b/public/temp/EX01_Financial Status Report.xlsx differ diff --git a/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx b/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx new file mode 100644 index 0000000..d1d54d8 --- /dev/null +++ b/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx @@ -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 ( + <> + + }> + + + + + ); +}; + +export default ProjectCashFlowReport; diff --git a/src/app/(main)/analytics/FinancialStatusReport/page.tsx b/src/app/(main)/analytics/FinancialStatusReport/page.tsx new file mode 100644 index 0000000..0a3865d --- /dev/null +++ b/src/app/(main)/analytics/FinancialStatusReport/page.tsx @@ -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 ( + + + Financial Status Report + + {/* }> + + */} + + + ); +}; +export default ProjectFinancialStatusReport; diff --git a/src/app/(main)/analytics/ProjectClaimsReport/page.tsx b/src/app/(main)/analytics/ProjectClaimsReport/page.tsx new file mode 100644 index 0000000..9f97f39 --- /dev/null +++ b/src/app/(main)/analytics/ProjectClaimsReport/page.tsx @@ -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 ( + + + Project Claims Report + + {/* }> + + */} + + + ); +}; +export default ProjectClaimsReport; diff --git a/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx b/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx new file mode 100644 index 0000000..5dc2f77 --- /dev/null +++ b/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx @@ -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 ( + + + Project Resource Summary + + }> + + + + ); +}; +export default ProjectResourceSummary; diff --git a/src/app/(main)/dashboard/StaffUtilization/page.tsx b/src/app/(main)/dashboard/StaffUtilization/page.tsx index 2ddea02..87bb6c0 100644 --- a/src/app/(main)/dashboard/StaffUtilization/page.tsx +++ b/src/app/(main)/dashboard/StaffUtilization/page.tsx @@ -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 = () => { diff --git a/src/app/(main)/projects/edit/not-found.tsx b/src/app/(main)/projects/edit/not-found.tsx new file mode 100644 index 0000000..14e0e6d --- /dev/null +++ b/src/app/(main)/projects/edit/not-found.tsx @@ -0,0 +1,17 @@ +import { getServerI18n } from "@/i18n"; +import { Stack, Typography, Link } from "@mui/material"; +import NextLink from "next/link"; + +export default async function NotFound() { + const { t } = await getServerI18n("projects", "common"); + + return ( + + {t("Not Found")} + {t("The project was not found!")} + + {t("Return to all projects")} + + + ); +} diff --git a/src/app/(main)/projects/edit/page.tsx b/src/app/(main)/projects/edit/page.tsx new file mode 100644 index 0000000..78e0ed1 --- /dev/null +++ b/src/app/(main)/projects/edit/page.tsx @@ -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 = 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 ( + <> + {t("Edit Project")} + + + + + ); +}; + +export default Projects; diff --git a/src/app/(main)/settings/customer/create/page.tsx b/src/app/(main)/settings/customer/create/page.tsx index e0dc0e0..13b38eb 100644 --- a/src/app/(main)/settings/customer/create/page.tsx +++ b/src/app/(main)/settings/customer/create/page.tsx @@ -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 () => { <> {t("Create Customer")} - + ); diff --git a/src/app/(main)/settings/customer/edit/page.tsx b/src/app/(main)/settings/customer/edit/page.tsx index 004781b..7755403 100644 --- a/src/app/(main)/settings/customer/edit/page.tsx +++ b/src/app/(main)/settings/customer/edit/page.tsx @@ -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 () => { <> {t("Edit Customer")} - + ); diff --git a/src/app/(main)/settings/skill/create/page.tsx b/src/app/(main)/settings/skill/create/page.tsx new file mode 100644 index 0000000..c98f993 --- /dev/null +++ b/src/app/(main)/settings/skill/create/page.tsx @@ -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 ( + <> + {t("Create Skill")} + + + + + ); +}; + +export default CreateStaff; diff --git a/src/app/(main)/settings/skill/page.tsx b/src/app/(main)/settings/skill/page.tsx new file mode 100644 index 0000000..f263c87 --- /dev/null +++ b/src/app/(main)/settings/skill/page.tsx @@ -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 ( + <> + + + {t("Skill")} + + + + + }> + + + + + ); +}; + +export default Skill; diff --git a/src/app/(main)/settings/user/page.tsx b/src/app/(main)/settings/user/page.tsx new file mode 100644 index 0000000..95973ab --- /dev/null +++ b/src/app/(main)/settings/user/page.tsx @@ -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 ( + <> + + + {t("User")} + + + + + }> + + + + + ); + }; + + export default User; \ No newline at end of file diff --git a/src/app/(main)/staffReimbursement/create/page.tsx b/src/app/(main)/staffReimbursement/create/page.tsx index f1effc4..7633135 100644 --- a/src/app/(main)/staffReimbursement/create/page.tsx +++ b/src/app/(main)/staffReimbursement/create/page.tsx @@ -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 () => { <> {t("Create Claim")} - + ); diff --git a/src/app/(main)/tasks/create/page.tsx b/src/app/(main)/tasks/create/page.tsx index 656139f..262f624 100644 --- a/src/app/(main)/tasks/create/page.tsx +++ b/src/app/(main)/tasks/create/page.tsx @@ -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 ( <> {t("Create Task Template")} - + + + ); }; diff --git a/src/app/(main)/tasks/edit/page.tsx b/src/app/(main)/tasks/edit/page.tsx new file mode 100644 index 0000000..2b2c02c --- /dev/null +++ b/src/app/(main)/tasks/edit/page.tsx @@ -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 ( + <> + {t("Edit Task Template")} + + + + + ); +}; + +export default TaskTemplates; diff --git a/src/app/(main)/tasks/page.tsx b/src/app/(main)/tasks/page.tsx index b9e9bf8..bf06dc2 100644 --- a/src/app/(main)/tasks/page.tsx +++ b/src/app/(main)/tasks/page.tsx @@ -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")} - }> - - + + }> + + + ); }; diff --git a/src/app/api/claims/actions.ts b/src/app/api/claims/actions.ts index d607c48..542effe 100644 --- a/src/app/api/claims/actions.ts +++ b/src/app/api/claims/actions.ts @@ -21,7 +21,7 @@ export interface ClaimDetailTable { id: number; invoiceDate: Date; description: string; - project: ProjectCombo; + project: number; amount: number; supportingDocumentName: string; oldSupportingDocument: SupportingDocument; diff --git a/src/app/api/clientprojects/index.ts b/src/app/api/clientprojects/index.ts index 5c65810..3eed422 100644 --- a/src/app/api/clientprojects/index.ts +++ b/src/app/api/clientprojects/index.ts @@ -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", diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts index 232f863..e383e89 100644 --- a/src/app/api/projects/actions.ts +++ b/src/app/api/projects/actions.ts @@ -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(`${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 }; diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index 0c7209c..90b0e10 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -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( + `${BASE_API_URL}/projects/assignedProjects`, + { + next: { tags: ["assignedProjects"] }, + }, + ); +}); + +export const fetchProjectDetails = cache(async (projectId: string) => { + return serverFetchJson( + `${BASE_API_URL}/projects/projectDetails/${projectId}`, + { + next: { tags: [`projectDetails_${projectId}`] }, + }, + ); +}); diff --git a/src/app/api/report7/index.ts b/src/app/api/report7/index.ts new file mode 100644 index 0000000..c31a754 --- /dev/null +++ b/src/app/api/report7/index.ts @@ -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", + }, +]; diff --git a/src/app/api/reporte1/index.ts b/src/app/api/reporte1/index.ts new file mode 100644 index 0000000..5e27648 --- /dev/null +++ b/src/app/api/reporte1/index.ts @@ -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", + }, +]; diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts new file mode 100644 index 0000000..be74c6f --- /dev/null +++ b/src/app/api/reports/actions.ts @@ -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( + `${BASE_API_URL}/reports/EX02-ProjectCashFlowReport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; \ No newline at end of file diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts new file mode 100644 index 0000000..6baa7aa --- /dev/null +++ b/src/app/api/reports/index.ts @@ -0,0 +1,8 @@ +// EX02 - Project Cash Flow Report +export interface EX02ProjectCashFlowReportFilter { + project: string[]; +} + +export interface EX02ProjectCashFlowReportRequest { + projectId: number; +} \ No newline at end of file diff --git a/src/app/api/resourcesummary/index.ts b/src/app/api/resourcesummary/index.ts new file mode 100644 index 0000000..ffaba69 --- /dev/null +++ b/src/app/api/resourcesummary/index.ts @@ -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', + }, +]; diff --git a/src/app/api/skill/actions.ts b/src/app/api/skill/actions.ts index eda7f39..6a0deca 100644 --- a/src/app/api/skill/actions.ts +++ b/src/app/api/skill/actions.ts @@ -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(`${BASE_API_URL}/skill/combo`, { next: { tags: ["skill"] }, }); - }); \ No newline at end of file + }); + + +export const saveSkill = async (data: CreateSkillInputs) => { + return serverFetchJson(`${BASE_API_URL}/skill/save`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + }; \ No newline at end of file diff --git a/src/app/api/skill/index.ts b/src/app/api/skill/index.ts new file mode 100644 index 0000000..cf6ebec --- /dev/null +++ b/src/app/api/skill/index.ts @@ -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(`${BASE_API_URL}/skill`, { + next: { tags: ["sill"] }, + }); + }); \ No newline at end of file diff --git a/src/app/api/staff/actions.ts b/src/app/api/staff/actions.ts index 9416d2d..a2235d6 100644 --- a/src/app/api/staff/actions.ts +++ b/src/app/api/staff/actions.ts @@ -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" }, diff --git a/src/app/api/tasks/actions.ts b/src/app/api/tasks/actions.ts index 862cc62..2c043be 100644 --- a/src/app/api/tasks/actions.ts +++ b/src/app/api/tasks/actions.ts @@ -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( - `${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( + `${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 +}; diff --git a/src/app/api/team/actions.ts b/src/app/api/team/actions.ts index 28496d0..47e1a82 100644 --- a/src/app/api/team/actions.ts +++ b/src/app/api/team/actions.ts @@ -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" }, }); }; diff --git a/src/app/api/team/index.ts b/src/app/api/team/index.ts index 3aa872d..d9d3b27 100644 --- a/src/app/api/team/index.ts +++ b/src/app/api/team/index.ts @@ -15,6 +15,7 @@ export interface TeamResult { staffName: string; posLabel: string; posCode: string; + teamLead: number; } diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts new file mode 100644 index 0000000..5df734a --- /dev/null +++ b/src/app/api/user/actions.ts @@ -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(`${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" }, + }); + }; \ No newline at end of file diff --git a/src/app/api/user/index.ts b/src/app/api/user/index.ts new file mode 100644 index 0000000..9a6065b --- /dev/null +++ b/src/app/api/user/index.ts @@ -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(`${BASE_API_URL}/user`, { + next: { tags: ["user"] }, + }); + }); \ No newline at end of file diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts index d4c71b6..72d4a56 100644 --- a/src/app/utils/commonUtil.ts +++ b/src/app/utils/commonUtil.ts @@ -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(); } \ No newline at end of file diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 0aaa798..a519164 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -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(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(...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(...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"); diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 314ea63..a94670a 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -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 = () => { diff --git a/src/components/ClaimDetail/index.ts b/src/components/ClaimDetail/index.ts deleted file mode 100644 index 0fa3ab2..0000000 --- a/src/components/ClaimDetail/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ClaimDetailWrapper"; diff --git a/src/components/ClaimDetail/ClaimFormInfo.tsx b/src/components/ClaimSave/ClaimFormInfo.tsx similarity index 100% rename from src/components/ClaimDetail/ClaimFormInfo.tsx rename to src/components/ClaimSave/ClaimFormInfo.tsx diff --git a/src/components/ClaimDetail/ClaimFormInputGrid.tsx b/src/components/ClaimSave/ClaimFormInputGrid.tsx similarity index 98% rename from src/components/ClaimDetail/ClaimFormInputGrid.tsx rename to src/components/ClaimSave/ClaimFormInputGrid.tsx index 24807bd..6aac620 100644 --- a/src/components/ClaimDetail/ClaimFormInputGrid.tsx +++ b/src/components/ClaimSave/ClaimFormInputGrid.tsx @@ -371,20 +371,21 @@ const ClaimFormInputGrid: React.FC = ({ 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 }, }, { diff --git a/src/components/ClaimDetail/ClaimDetail.tsx b/src/components/ClaimSave/ClaimSave.tsx similarity index 93% rename from src/components/ClaimDetail/ClaimDetail.tsx rename to src/components/ClaimSave/ClaimSave.tsx index db74447..336d554 100644 --- a/src/components/ClaimDetail/ClaimDetail.tsx +++ b/src/components/ClaimSave/ClaimSave.tsx @@ -21,7 +21,7 @@ export interface Props { projectCombo: ProjectCombo[] } -const ClaimDetail: React.FC = ({ projectCombo }) => { +const ClaimSave: React.FC = ({ projectCombo }) => { const { t } = useTranslation("common"); const [serverError, setServerError] = useState(""); const router = useRouter(); @@ -74,14 +74,15 @@ const ClaimDetail: React.FC = ({ 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 = ({ projectCombo }) => { ); }; -export default ClaimDetail; +export default ClaimSave; diff --git a/src/components/ClaimDetail/ClaimDetailWrapper.tsx b/src/components/ClaimSave/ClaimSaveWrapper.tsx similarity index 82% rename from src/components/ClaimDetail/ClaimDetailWrapper.tsx rename to src/components/ClaimSave/ClaimSaveWrapper.tsx index 602f9e3..af6234f 100644 --- a/src/components/ClaimDetail/ClaimDetailWrapper.tsx +++ b/src/components/ClaimSave/ClaimSaveWrapper.tsx @@ -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 ( - + ); }; diff --git a/src/components/ClaimSave/index.ts b/src/components/ClaimSave/index.ts new file mode 100644 index 0000000..56eabaf --- /dev/null +++ b/src/components/ClaimSave/index.ts @@ -0,0 +1 @@ +export { default } from "./ClaimSaveWrapper"; diff --git a/src/components/ClaimSearch/ClaimSearch.tsx b/src/components/ClaimSearch/ClaimSearch.tsx index c0ab01f..304993a 100644 --- a/src/components/ClaimSearch/ClaimSearch.tsx +++ b/src/components/ClaimSearch/ClaimSearch.tsx @@ -50,12 +50,12 @@ const ClaimSearch: React.FC = ({ claims }) => { const columns = useMemo[]>( () => [ - // { - // name: "action", - // label: t("Actions"), - // onClick: onClaimClick, - // buttonIcon: , - // }, + { + name: "id", + label: t("Details"), + onClick: onClaimClick, + buttonIcon: , + }, { name: "created", label: t("Creation Date"), type: "date" }, { name: "code", label: t("Claim Code") }, // { name: "project", label: t("Related Project Name") }, diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 3e1a3f0..fb57dd7 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -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 = ({ + isEditMode, + defaultInputs, allTasks, projectCategories, taskTemplates, @@ -90,9 +98,22 @@ const CreateProject: React.FC = ({ 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>( (_e, newValue) => { setTabIndex(newValue); @@ -101,16 +122,53 @@ const CreateProject: React.FC = ({ ); const onSubmit = useCallback>( - 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>( @@ -133,85 +191,112 @@ const CreateProject: React.FC = ({ 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 ( - - - - - ) : undefined - } - iconPosition="end" - /> - - - - - { - - } - { - - } - { - - } - {} - {serverError && ( - - {serverError} - - )} - - } + {formProps.getValues("projectActualStart") && !formProps.getValues("projectActualEnd") && } + {!(formProps.getValues("projectActualStart") && formProps.getValues("projectActualEnd")) && } + + )} + - {t("Cancel")} - - + + ) : undefined + } + iconPosition="end" + /> + + + + + { + + } + { + + } + { + + } + {} + {serverError && ( + + {serverError} + + )} + + + + - - + + ); }; diff --git a/src/components/CreateProject/CreateProjectWrapper.tsx b/src/components/CreateProject/CreateProjectWrapper.tsx index 3ca2fae..ab9c830 100644 --- a/src/components/CreateProject/CreateProjectWrapper.tsx +++ b/src/components/CreateProject/CreateProjectWrapper.tsx @@ -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 = 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 ( = () => { + const formProps = useForm(); + const [serverError, setServerError] = useState(""); + const router = useRouter(); + const { t } = useTranslation(); + const [tabIndex, setTabIndex] = useState(0); + const errors = formProps.formState.errors; + + const onSubmit = useCallback>( + 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>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [] + ); + + const hasErrorsInTab = ( + tabIndex: number, + errors: FieldErrors + ) => { + switch (tabIndex) { + case 0: + return Object.keys(errors).length > 0; + default: + false; + } + }; + return ( + <> + + + + + ) : undefined + } + iconPosition="end" + /> + {/* */} + + {serverError && ( + + {serverError} + + )} + {tabIndex === 0 && } + + + + + + + + ); +}; + +export default CreateSkill; diff --git a/src/components/CreateSkill/CreateSkillLoading.tsx b/src/components/CreateSkill/CreateSkillLoading.tsx new file mode 100644 index 0000000..f7d17bf --- /dev/null +++ b/src/components/CreateSkill/CreateSkillLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const CreateSkillLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + CreateSkill + + + + + + + + + + + ); +}; + +export default CreateSkillLoading; diff --git a/src/components/CreateSkill/CreateSkillWrapper.tsx b/src/components/CreateSkill/CreateSkillWrapper.tsx new file mode 100644 index 0000000..f2f667a --- /dev/null +++ b/src/components/CreateSkill/CreateSkillWrapper.tsx @@ -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 ; +}; + +CreateSkillWrapper.Loading = CreateSkillLoading; + +export default CreateSkillWrapper; diff --git a/src/components/CreateSkill/SkillInfo.tsx b/src/components/CreateSkill/SkillInfo.tsx new file mode 100644 index 0000000..be9724d --- /dev/null +++ b/src/components/CreateSkill/SkillInfo.tsx @@ -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(); + + const resetSkill = useCallback(() => { + console.log(defaultValues); + if (defaultValues !== undefined) { + resetField("name"); + } + }, [defaultValues]); + + return ( + <> + + + + + {t("Skill Info")} + + + + + + + + + + + + + + + + + ); +}; +export default SkillInfo; diff --git a/src/components/CreateSkill/index.ts b/src/components/CreateSkill/index.ts new file mode 100644 index 0000000..044c4cf --- /dev/null +++ b/src/components/CreateSkill/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateSkillWrapper"; diff --git a/src/components/CreateStaff/CreateStaff.tsx b/src/components/CreateStaff/CreateStaff.tsx index 312f18e..94e7f71 100644 --- a/src/components/CreateStaff/CreateStaff.tsx +++ b/src/components/CreateStaff/CreateStaff.tsx @@ -190,7 +190,7 @@ const CreateStaff: React.FC = ({ Title }) => { { id: "skillSetId", label: t("Skillset"), - type: "combo-Obj", + type: "multiSelect-Obj", options: skillCombo || [], required: false, }, diff --git a/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx b/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx index 066c994..f7d5912 100644 --- a/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx +++ b/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx @@ -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 = ({ tasks }) => { const { t } = useTranslation(); + const searchParams = useSearchParams() const router = useRouter(); const handleCancel = () => { router.back(); @@ -49,6 +52,7 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { handleSubmit, setValue, watch, + resetField, formState: { errors, isSubmitting }, } = useForm({ defaultValues: { taskIds: [] } }); @@ -57,12 +61,56 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { return items.filter((item) => currentTaskIds.includes(item.id)); }, [currentTaskIds, items]); + const [refTaskTemplate, setRefTaskTemplate] = React.useState() + const id = searchParams.get('id') + + const fetchCurrentTaskTemplate = async () => { + try { + const taskTemplate = await fetchTaskTemplate(parseInt(id!!)) + + const defaultValues = { + id: parseInt(id!!), + code: taskTemplate.code ?? null, + name: taskTemplate.name ?? null, + taskIds: taskTemplate.tasks.map(task => task.id) ?? [], + } + + setRefTaskTemplate(defaultValues) + } catch (e) { + console.log(e) + } + } + + React.useLayoutEffect(() => { + if (id !== null && parseInt(id) > 0) fetchCurrentTaskTemplate() + }, [id]) + + React.useEffect(() => { + if (refTaskTemplate) { + setValue("taskIds", refTaskTemplate.taskIds) + resetField("code", { defaultValue: refTaskTemplate.code }) + resetField("name", { defaultValue: refTaskTemplate.name }) + setValue("id", refTaskTemplate.id) + } + }, [refTaskTemplate]) + const onSubmit: SubmitHandler = 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 = ({ tasks }) => { ); return ( - - - - {t("Task List Setup")} - - - - - - + { + (id === null || refTaskTemplate !== undefined) && + + + {t("Task List Setup")} + + + + + + + + + { + setValue( + "taskIds", + selectedTasks.map((item) => item.id), + ); + }} + allItemsLabel={t("Task Pool")} + selectedItemsLabel={t("Task List Template")} /> - - - { - setValue( - "taskIds", - selectedTasks.map((item) => item.id), - ); - }} - allItemsLabel={t("Task Pool")} - selectedItemsLabel={t("Task List Template")} - /> - - - {serverError && ( - - {serverError} - - )} - - - - - + + + { + serverError && ( + + {serverError} + + ) + } + + + + + } + ); }; diff --git a/src/components/CreateTeam/CreateTeam.tsx b/src/components/CreateTeam/CreateTeam.tsx index 93b585e..64159c0 100644 --- a/src/components/CreateTeam/CreateTeam.tsx +++ b/src/components/CreateTeam/CreateTeam.tsx @@ -89,7 +89,7 @@ const hasErrorsInTab = ( } iconPosition="end" /> - + {serverError && ( diff --git a/src/components/CreateTeam/StaffAllocation.tsx b/src/components/CreateTeam/StaffAllocation.tsx index c51b839..bbd768c 100644 --- a/src/components/CreateTeam/StaffAllocation.tsx +++ b/src/components/CreateTeam/StaffAllocation.tsx @@ -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 = ({ allStaffs: staff }) => { reset, resetField, } = useFormContext(); - + const initialStaffs = staff.map((s) => ({ ...s })); -// console.log(initialStaffs) + // console.log(initialStaffs) const [filteredStaff, setFilteredStaff] = useState(initialStaffs); const [selectedStaff, setSelectedStaff] = useState( initialStaffs.filter((s) => getValues("addStaffIds")?.includes(s.id)) ); - const [seletedTeamLead, setSeletedTeamLead] = useState() - // Adding / Removing staff + // Adding / Removing staff const addStaff = useCallback((staff: StaffResult) => { setSelectedStaff((s) => [...s, staff]); }, []); @@ -53,27 +64,31 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); }, []); - const setTeamLead = useCallback((staff: StaffResult) => { - - setSeletedTeamLead(staff.id) - const rearrangedList = getValues("addStaffIds").reduce((acc, num, index) => { - if (num === staff.id && index !== 0) { + const setTeamLead = useCallback( + (staff: StaffResult) => { + const rearrangedList = getValues("addStaffIds").reduce( + (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 = ({ 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 = ({ allStaffs: staff }) => { }, [selectedStaff, setValue]); useEffect(() => { - console.log(selectedStaff) + console.log(selectedStaff); }, [selectedStaff]); const StaffPoolColumns = useMemo[]>( @@ -107,7 +122,7 @@ const StaffAllocation: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ allStaffs: staff }) => { (_e, newValue) => { setTabIndex(newValue); }, - [], + [] ); return ( @@ -185,48 +199,48 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { {t("staff")} - - - - - - - - ), - }} - /> + + + + + + + + ), + }} + /> + - - - - - - - {tabIndex === 0 && ( - - )} - {tabIndex === 1 && ( - + + - )} - + + + {tabIndex === 0 && ( + + )} + {tabIndex === 1 && ( + + )} + diff --git a/src/components/CustomDatagrid/CustomDatagrid.tsx b/src/components/CustomDatagrid/CustomDatagrid.tsx index 314ba6c..4867623 100644 --- a/src/components/CustomDatagrid/CustomDatagrid.tsx +++ b/src/components/CustomDatagrid/CustomDatagrid.tsx @@ -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 = ({ @@ -32,6 +34,8 @@ const CustomDatagrid: React.FC = ({ 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 = ({ editMode="row" checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} initialState={{ pagination: { paginationModel: { pageSize: 10 } }, }} @@ -222,6 +228,8 @@ const CustomDatagrid: React.FC = ({ editMode="row" checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} initialState={{ pagination: { paginationModel: { pageSize: 10 } }, }} @@ -251,6 +259,8 @@ const CustomDatagrid: React.FC = ({ 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 = ({ 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 = ({ "& .MuiDataGrid-cell:hover": { color: "primary.main", }, - height: 300, + height: dataGridHeight ?? 300, "& .MuiDataGrid-root": { overflow: "auto", }, diff --git a/src/components/CustomInputForm/CustomInputForm.tsx b/src/components/CustomInputForm/CustomInputForm.tsx index 9497208..417c6f1 100644 --- a/src/components/CustomInputForm/CustomInputForm.tsx +++ b/src/components/CustomInputForm/CustomInputForm.tsx @@ -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 = ({ ); } else if (field.type === "multiDate") { - console.log(dayjs(field.value)) + // console.log(dayjs(field.value)) return ( @@ -343,8 +345,6 @@ const CustomInputForm: React.FC = ({ 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 = ({ ); + } else if (field.type === "multiSelect-Obj") { + return ( + + + {field.label} + ( + + )} + /> + + + ); } else if (field.type === "numeric") { return ( diff --git a/src/components/CustomerDetail/index.ts b/src/components/CustomerDetail/index.ts deleted file mode 100644 index a8811e6..0000000 --- a/src/components/CustomerDetail/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./CustomerDetailWrapper"; \ No newline at end of file diff --git a/src/components/CustomerDetail/ContactInfo.tsx b/src/components/CustomerSave/ContactInfo.tsx similarity index 100% rename from src/components/CustomerDetail/ContactInfo.tsx rename to src/components/CustomerSave/ContactInfo.tsx diff --git a/src/components/CustomerDetail/CustomerInfo.tsx b/src/components/CustomerSave/CustomerInfo.tsx similarity index 100% rename from src/components/CustomerDetail/CustomerInfo.tsx rename to src/components/CustomerSave/CustomerInfo.tsx diff --git a/src/components/CustomerDetail/CustomerDetail.tsx b/src/components/CustomerSave/CustomerSave.tsx similarity index 92% rename from src/components/CustomerDetail/CustomerDetail.tsx rename to src/components/CustomerSave/CustomerSave.tsx index 88a99ad..acb4ecf 100644 --- a/src/components/CustomerDetail/CustomerDetail.tsx +++ b/src/components/CustomerSave/CustomerSave.tsx @@ -42,7 +42,7 @@ const hasErrorsInTab = ( } }; -const CustomerDetail: React.FC = ({ +const CustomerSave: React.FC = ({ subsidiaries, customerTypes, }) => { @@ -199,20 +199,20 @@ const CustomerDetail: React.FC = ({ 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 = ({ ); }; -export default CustomerDetail; \ No newline at end of file +export default CustomerSave; \ No newline at end of file diff --git a/src/components/CustomerDetail/CustomerDetailWrapper.tsx b/src/components/CustomerSave/CustomerSaveWrapper.tsx similarity index 73% rename from src/components/CustomerDetail/CustomerDetailWrapper.tsx rename to src/components/CustomerSave/CustomerSaveWrapper.tsx index 0206940..078f50a 100644 --- a/src/components/CustomerDetail/CustomerDetailWrapper.tsx +++ b/src/components/CustomerSave/CustomerSaveWrapper.tsx @@ -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 ( - + ); }; -export default CustomerDetailWrapper; +export default CustomerSaveWrapper; diff --git a/src/components/CustomerDetail/SubsidiaryAllocation.tsx b/src/components/CustomerSave/SubsidiaryAllocation.tsx similarity index 100% rename from src/components/CustomerDetail/SubsidiaryAllocation.tsx rename to src/components/CustomerSave/SubsidiaryAllocation.tsx diff --git a/src/components/CustomerSave/index.ts b/src/components/CustomerSave/index.ts new file mode 100644 index 0000000..ea74d25 --- /dev/null +++ b/src/components/CustomerSave/index.ts @@ -0,0 +1 @@ +export { default } from "./CustomerSaveWrapper"; \ No newline at end of file diff --git a/src/components/EditStaff/EditStaff.tsx b/src/components/EditStaff/EditStaff.tsx index bcc7346..0d62e28 100644 --- a/src/components/EditStaff/EditStaff.tsx +++ b/src/components/EditStaff/EditStaff.tsx @@ -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": diff --git a/src/components/EditTeam/Allocation.tsx b/src/components/EditTeam/Allocation.tsx index 44c89b1..2599867 100644 --- a/src/components/EditTeam/Allocation.tsx +++ b/src/components/EditTeam/Allocation.tsx @@ -35,9 +35,10 @@ import StarsIcon from "@mui/icons-material/Stars"; export interface Props { allStaffs: StaffResult[]; + teamLead: number; } -const Allocation: React.FC = ({ allStaffs: staff }) => { +const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { const { t } = useTranslation(); const searchParams = useSearchParams(); const idString = searchParams.get("id"); @@ -53,9 +54,16 @@ const Allocation: React.FC = ({ allStaffs: staff }) => { const initialStaffs = staff.map((s) => ({ ...s })); const [filteredStaff, setFilteredStaff] = useState(initialStaffs); - const [selectedStaff, setSelectedStaff] = useState( - filteredStaff.filter((s) => getValues("addStaffIds")?.includes(s.id)) + const [selectedStaff, setSelectedStaff] = useState(() => { + 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(); const [deletedStaffIds, setDeletedStaffIds] = useState([]); @@ -84,8 +92,8 @@ const Allocation: React.FC = ({ 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); diff --git a/src/components/EditTeam/EditTeam.tsx b/src/components/EditTeam/EditTeam.tsx index 095559b..432bc0e 100644 --- a/src/components/EditTeam/EditTeam.tsx +++ b/src/components/EditTeam/EditTeam.tsx @@ -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 = async ({ staff, desc }) => { @@ -37,6 +40,8 @@ const EditTeam: React.FC = async ({ staff, desc }) => { const [filteredItems, setFilteredItems] = useState(); const [allStaffs, setAllStaffs] = useState(); const [filteredDesc, setFilteredDesc] = useState(); + const [filteredName, setFilteredName] = useState(); + const [teamLead, setTeamLead] = useState(); const [tabIndex, setTabIndex] = useState(0); const router = useRouter(); // const [selectedStaff, setSelectedStaff] = useState( @@ -63,25 +68,47 @@ const EditTeam: React.FC = 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 = async ({ staff, desc }) => { > - {t("Edit Team")} + {t("Edit Team")} - {filteredName} = async ({ staff, desc }) => { {tabIndex === 0 && } - {tabIndex === 1 && } + {tabIndex === 1 && } + + + + + ); +} + +export default SearchBox; diff --git a/src/components/Report/ReportSearchBox7/index.ts b/src/components/Report/ReportSearchBox7/index.ts new file mode 100644 index 0000000..b3a9815 --- /dev/null +++ b/src/components/Report/ReportSearchBox7/index.ts @@ -0,0 +1,3 @@ +//src\components\SearchBox\index.ts +export { default } from "./SearchBox7"; +export type { Criterion } from "./SearchBox7"; diff --git a/src/components/Report/ReportSearchBox8/SearchBox8.tsx b/src/components/Report/ReportSearchBox8/SearchBox8.tsx new file mode 100644 index 0000000..d4e1033 --- /dev/null +++ b/src/components/Report/ReportSearchBox8/SearchBox8.tsx @@ -0,0 +1,358 @@ +//src\components\ReportSearchBox\SearchBox.tsx +"use client"; + +import Grid from "@mui/material/Grid"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import React, { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import TextField from "@mui/material/TextField"; +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import CardActions from "@mui/material/CardActions"; +import Button from "@mui/material/Button"; +import RestartAlt from "@mui/icons-material/RestartAlt"; +import Search from "@mui/icons-material/Search"; +import dayjs from "dayjs"; +import "dayjs/locale/zh-hk"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { Box } from "@mui/material"; +import * as XLSX from 'xlsx-js-style'; +//import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton'; + +interface BaseCriterion { + label: string; + label2?: string; + paramName: T; + paramName2?: T; +} + +interface TextCriterion extends BaseCriterion { + type: "text"; +} + +interface SelectCriterion extends BaseCriterion { + type: "select"; + options: string[]; +} + +interface DateRangeCriterion extends BaseCriterion { + type: "dateRange"; +} + +export type Criterion = + | TextCriterion + | SelectCriterion + | DateRangeCriterion; + +interface Props { + criteria: Criterion[]; + onSearch: (inputs: Record) => void; + onReset?: () => void; +} + +function SearchBox({ + criteria, + onSearch, + onReset, +}: Props) { + const { t } = useTranslation("common"); + const defaultInputs = useMemo( + () => + criteria.reduce>( + (acc, c) => { + return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; + }, + {} as Record, + ), + [criteria], + ); + const [inputs, setInputs] = useState(defaultInputs); + + const makeInputChangeHandler = useCallback( + (paramName: T): React.ChangeEventHandler => { + return (e) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, + [], + ); + + const makeSelectChangeHandler = useCallback((paramName: T) => { + return (e: SelectChangeEvent) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, []); + + const makeDateChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); + }; + }, []); + + const makeDateToChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ + ...i, + [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), + })); + }; + }, []); + + const handleReset = () => { + setInputs(defaultInputs); + onReset?.(); + }; + + const handleSearch = () => { + onSearch(inputs); + + }; + + const handleDownload = async () => { + //setIsLoading(true); + + try { + const response = await fetch('/temp/AR08_Project P&L Report.xlsx', { + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }, + }); + if (!response.ok) throw new Error('Network response was not ok.'); + + const data = await response.blob(); + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target && e.target.result) { + const ab = e.target.result as ArrayBuffer; + const workbook = XLSX.read(ab, { type: 'array' }); + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + + // Add the current date to cell B2 + const cellAddress = 'B2'; + const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD + const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD + XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); + + // Style for cell A1: Font size 16 and bold + if (worksheet['A1']) { + worksheet['A1'].s = { + font: {bold: true,sz: 16, // Font size 16 + alignment: { horizontal: 'left' }, + //name: 'Times New Roman' // Specify font + } + }; + } + + // Apply styles from A2 to A7 (bold) + ['A2', 'A3', 'A4','A5', 'A6', 'A7'].forEach(cell => { + if (worksheet[cell]) { + worksheet[cell].s = { font: { bold: true,sz: 12, + alignment: { horizontal: 'left' }, } + }; + } + }); + + const firstTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 + ['Data1', 'Data2', 'Data3'], // Row 2 + // ... more rows as needed + ]; + const secondTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 of second table + ['Data1', 'Data2', 'Data3'], // Row 2 of second table + // ... more rows as needed + ]; + + // Find the last row of the first table + let lastRowOfFirstTable = 10; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + + // Insert the first data form into the worksheet at the desired location + XLSX.utils.sheet_add_aoa(worksheet, firstTableData, { origin: { c: 0, r: lastRowOfFirstTable } }); + // Update lastRowOfFirstTable to account for the new data + lastRowOfFirstTable += firstTableData.length; + // Now insert the text that goes between the two tables + + // // Insert the additional text with one row of spacing after the first table + const textRow = lastRowOfFirstTable + 1; // Adjust the 1 based on how many lines of spacing you want + XLSX.utils.sheet_add_aoa(worksheet, [['Staff No. and Name']], { origin: { c: 0, r: textRow+1 } }); + + + let secondTableStartRow = textRow + 3; + // Insert the second data form into the worksheet at the new starting row + XLSX.utils.sheet_add_aoa(worksheet, secondTableData, { origin: { c: 0, r: secondTableStartRow } }); + + + // Source cell coordinates + const sourceCellCoord = { c: 1, r: 2 }; // C3 (columns and rows are 0-indexed in this library) + // Target cell coordinates + const targetCellCoord = { c: 3, r: 9 }; + // Create references for source and target cells + const sourceCellRef = XLSX.utils.encode_cell(sourceCellCoord); + const targetCellRef = XLSX.utils.encode_cell(targetCellCoord); + // Copy the cell content from C3 to the target cell + if (worksheet[sourceCellRef]) { + worksheet[targetCellRef] = { ...worksheet[sourceCellRef] }; + // If the source cell has a style, deep clone it for the target cell + if (worksheet[sourceCellRef].s) { + worksheet[targetCellRef].s = JSON.parse(JSON.stringify(worksheet[sourceCellRef].s)); + } + } + + // Formatting from A10 to F10 + // Apply styles from A10 to F10 (bottom border, center alignment) + for (let col = 0; col < 7; col++) { // Columns A to G + const cellRef = XLSX.utils.encode_col(col) + '10';//row + if (worksheet[cellRef]) { + worksheet[cellRef].s = { + font: { bold: false }, + alignment: { horizontal: 'center' }, + border: { + bottom: { style: 'thin', color: { auto: 1 } } + } + }; + } + } + + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + // Start with a base width for each column (optional, but can help with columns that have no data) + const maxCol = worksheet['!ref'] ? worksheet['!ref'].split(':')[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1 : 0; + for (let col = 0; col < maxCol; col++) { + colWidths[col] = 10; // Default base width + } + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + // Skip the first row in the jsonData + for (let row = 1; row < jsonData.length; row++) { + jsonData[row].forEach((cell, index) => { + // Only process if the cell is not null/undefined + if (cell) { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + } + }); + } + + // Apply calculated widths to each column, skipping the first row + worksheet['!cols'] = colWidths.map((width, index) => { + return { wch: width + 2 }; // +2 for a little extra padding + }); + + + // Format filename with date + const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD + const filename = `AR08_Project_P&L_Report_${today}.xlsx`; // Append formatted date to the filename + + // Convert workbook back to XLSX file + XLSX.writeFile(workbook, filename); + } else { + throw new Error('Failed to load file'); + } + }; + reader.readAsArrayBuffer(data); + } catch (error) { + console.error('Error downloading the file: ', error); + } + + //setIsLoading(false); + }; + return ( + + + {t("Search Criteria")} + + {criteria.map((c) => { + return ( + + {c.type === "text" && ( + + )} + {c.type === "select" && ( + + {c.label} + + + )} + {c.type === "dateRange" && ( + + + + + + + {"-"} + + + + + + + )} + + ); + })} + + + + + + + + ); +} + +export default SearchBox; diff --git a/src/components/Report/ReportSearchBox8/index.ts b/src/components/Report/ReportSearchBox8/index.ts new file mode 100644 index 0000000..003fbde --- /dev/null +++ b/src/components/Report/ReportSearchBox8/index.ts @@ -0,0 +1,3 @@ +//src\components\SearchBox\index.ts +export { default } from "./SearchBox8"; +export type { Criterion } from "./SearchBox8"; diff --git a/src/components/Report/ReportSearchBoxe1/SearchBoxe1.tsx b/src/components/Report/ReportSearchBoxe1/SearchBoxe1.tsx new file mode 100644 index 0000000..ea7afb6 --- /dev/null +++ b/src/components/Report/ReportSearchBoxe1/SearchBoxe1.tsx @@ -0,0 +1,482 @@ +//src\components\ReportSearchBox\SearchBox2.tsx +"use client"; + +import Grid from "@mui/material/Grid"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import React, { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import TextField from "@mui/material/TextField"; +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import CardActions from "@mui/material/CardActions"; +import Button from "@mui/material/Button"; +import RestartAlt from "@mui/icons-material/RestartAlt"; +import Search from "@mui/icons-material/Search"; +import dayjs from "dayjs"; +import "dayjs/locale/zh-hk"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { Box } from "@mui/material"; +import * as XLSX from 'xlsx-js-style'; +//import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton'; + +interface BaseCriterion { + label: string; + label2?: string; + paramName: T; + paramName2?: T; +} + +interface TextCriterion extends BaseCriterion { + type: "text"; +} + +interface SelectCriterion extends BaseCriterion { + type: "select"; + options: string[]; +} + +interface DateRangeCriterion extends BaseCriterion { + type: "dateRange"; +} + +export type Criterion = + | TextCriterion + | SelectCriterion + | DateRangeCriterion; + +interface Props { + criteria: Criterion[]; + onSearch: (inputs: Record) => void; + onReset?: () => void; +} + +function SearchBox({ + criteria, + onSearch, + onReset, +}: Props) { + const { t } = useTranslation("common"); + const defaultInputs = useMemo( + () => + criteria.reduce>( + (acc, c) => { + return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; + }, + {} as Record, + ), + [criteria], + ); + const [inputs, setInputs] = useState(defaultInputs); + + const makeInputChangeHandler = useCallback( + (paramName: T): React.ChangeEventHandler => { + return (e) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, + [], + ); + + const makeSelectChangeHandler = useCallback((paramName: T) => { + return (e: SelectChangeEvent) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, []); + + const makeDateChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); + }; + }, []); + + const makeDateToChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ + ...i, + [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), + })); + }; + }, []); + + interface CellValue { + v: number | string; // Value of the cell + t: 'n' | 's'; // Type of the cell value: 'n' for number, 's' for string + s?: XLSX.CellStyle; // Optional style for the cell + } + + const handleReset = () => { + setInputs(defaultInputs); + onReset?.(); + }; + + const handleSearch = () => { + onSearch(inputs); + + }; + + // Function to merge cells from A2:B2 to A14:B14 +function mergeCells(worksheet: XLSX.WorkSheet) { + // Ensure the 'merges' array exists in the worksheet + if (!worksheet['!merges']) worksheet['!merges'] = []; + + // Loop through rows 2 to 14 (0-indexed + 1) + for (let row = 1; row <= 13; row++) { + // Define the range for current row to merge A and B columns + const mergeRange = { + s: { c: 0, r: row }, // Start cell (Column A) + e: { c: 1, r: row } // End cell (Column B) + }; + // Add the range to the 'merges' array in the worksheet + worksheet['!merges'].push(mergeRange); + // Apply center alignment to the merged cell + const mergedCellRef = XLSX.utils.encode_cell({ c: 0, r: row }); + if (!worksheet[mergedCellRef]) { + worksheet[mergedCellRef] = {}; // Create the cell if it doesn't exist + } + worksheet[mergedCellRef].s = { + alignment: { horizontal: "left", wrapText: true } + }; + } +} + +// Processing and inserting table data with calculations +function processDataAndInsert(worksheet: XLSX.WorkSheet, startRow:number, data:(string|number)[][]) { + data.forEach((row, rowIndex) => { + const r = startRow + rowIndex; + + // Direct assignments for columns A-F as strings + const stringCols = ['A', 'B', 'C', 'D', 'E', 'F']; + stringCols.forEach((col, index) => { + const cellRef = col + r; + worksheet[cellRef] = { v: row[index], t: 's' }; // Force type as string + }); + + // Assignments for columns G-O as numbers + const numberCols = ['G', 'H', 'I', 'K', 'N']; + const colIndices = [6, 7, 8, 9, 10]; // Indices in the data array corresponding to G, H, I, K, N + numberCols.forEach((col, index) => { + const cellRef = col + r; + worksheet[cellRef] = { v: row[colIndices[index]], t: 'n' }; // Force type as number + }); + + // Calculations for columns J, L, M, O + const h = row[6] as number; + const i = row[7] as number; + const k = row[9] as number; + const n = row[10] as number; + + // Column J: H - I + worksheet['J' + r] = { v: h - i, t: 'n' }; + + // Column L: IF(H { + const cellRefs = data.map((_, index) => col + (startRow + index)); + const formula = `=SUM(${cellRefs.join(',')})`; + worksheet[col + sumRow] = { f: formula, t: 'n', s: { + border: { + top: {style: 'thin', color: {auto: 1}}, + bottom: {style: 'double', color: {auto: 1}} + } + }}; + }); + XLSX.utils.sheet_add_aoa(worksheet, [['Sub-total']], { origin: { c: 0, r: (sumRow-1) } }); + +// const mergedCellRefA1 = XLSX.utils.encode_cell({ c: 0, r: sumRow-1}); +// if (!worksheet[mergedCellRefA1]) { +// worksheet[mergedCellRefA1] = {}; // Create the cell if it doesn't exist +// } +// // Apply right alignment, center vertical alignment, wrap text, and border styles to the 'Sub-total' cell +// worksheet[mergedCellRefA1].s = { +// alignment: {horizontal: "right", vertical: "center", wrapText: true}, +// border: { +// top: {style: 'thin', color: {auto: 1}}, +// bottom: {style: 'double', color: {auto: 1}} +// } +// }; +// Define the range of cells to merge for 'Sub-total' +const mergeRangeSubTotal = { + s: { c: 0, r: sumRow-1}, // Start at column A + e: { c: 5, r: sumRow-1} // End at column F +}; +// // Add the range to the 'merges' array in the worksheet if it doesn't exist +// if (!worksheet['!merges']) worksheet['!merges'] = []; +// worksheet['!merges'].push(mergeRangeSubTotal); + +// Update styles for the merged cell range where 'Sub-total' is located +const mergedCellRefSubTotal = XLSX.utils.encode_cell({ c: 0, r: sumRow-1 }); +if (!worksheet[mergedCellRefSubTotal]) { + worksheet[mergedCellRefSubTotal] = {}; // Create the cell if it doesn't exist +} +worksheet[mergedCellRefSubTotal].s = { + alignment: {horizontal: "right", vertical: "center", wrapText: true}, + border: { + top: {style: 'thin', color: {auto: 1}}, + bottom: {style: 'double', color: {auto: 1}}} +}; +// Add the range to the 'merges' array in the worksheet if it doesn't exist +if (!worksheet['!merges']) worksheet['!merges'] = []; +worksheet['!merges'].push(mergeRangeSubTotal) + + +const mergedCellRefM1 = XLSX.utils.encode_cell({ c: 12, r: sumRow}); +if (!worksheet[mergedCellRefM1]) { + worksheet[mergedCellRefM1] = {}; // Create the cell if it doesn't exist +} +worksheet[mergedCellRefM1].s = { + alignment: {horizontal: "right", vertical: "center", wrapText: true}, + border: { + top: {style: 'thin', color: {auto: 1}}, + bottom: {style: 'double', color: {auto: 1}} + } +}; + +} +const firstTableData = [ + ['Code1', 'PJName1', 'Client1','Team1','2011/01/01','2011/02/01','625','500','350','350','171'], // Row 1 + ['Code2', 'PJName2', 'Client2','Team2','2011/03/01','2011/04/01','1000','800','565','565','565'],// Row 2 + ['Code2', 'PJName2', 'Client2','Team2','2011/03/01','2011/04/01','1000','800','565','565','565'],// Row 3 + // ... more rows as needed +]; + + const handleDownload = async () => { + //setIsLoading(true); + + try { + const response = await fetch('/temp/EX01_Financial Status Report.xlsx', { + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }, + }); + if (!response.ok) throw new Error('Network response was not ok.'); + + const data = await response.blob(); + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target && e.target.result) { + const ab = e.target.result as ArrayBuffer; + const workbook = XLSX.read(ab, { type: 'array' }); + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + + // Add the current date to cell C2 + const cellAddress = 'C2'; + const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD + const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD + XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); + + mergeCells(worksheet); + + // Style for cell A1: Font size 16 and bold + if (worksheet['A1']) { + worksheet['A1'].s = { + font: {bold: true,sz: 16,},alignment: { horizontal: 'center' } // Font size 16 //name: 'Times New Roman' // Specify font + }; + } + + // Apply styles from A2 A3 A5 (bold) + ['A2', 'A3', 'A5','A14'].forEach(cell => { + if (worksheet[cell]) { + worksheet[cell].s = { font: { bold: true },alignment: { horizontal: 'left' } }; + } + }); + + // Apply styles from A2 A3 A5 (bold) + ['A6', 'A7', 'A8','A9','A10','A11','A12'].forEach(cell => { + if (worksheet[cell]) { + worksheet[cell].s = { font: { bold: false },alignment: { horizontal: 'left' } }; + } + }); + + // Formatting from A15 to O15 + // Apply styles from A6 to K6 (bold, bottom border, center alignment) + for (let col = 0; col < 15; col++) { // Columns A to O + const cellRef = XLSX.utils.encode_col(col) + '15'; + if (worksheet[cellRef]) { + worksheet[cellRef].s = { + font: { bold: true }, + alignment: { horizontal: 'center' }, + border: { + bottom: { style: 'thin', color: { auto: 1 } } + } + }; + } + } + + // Find the last row of the first table + let lastRowOfFirstTable = 16; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + // Insert the first data form into the worksheet at the desired location + //XLSX.utils.sheet_add_aoa(worksheet, firstTableData, { origin: { c: 0, r: lastRowOfFirstTable } }); + // Assuming worksheet is already defined, and we start inserting from row 16 +processDataAndInsert(worksheet, 16, firstTableData); + // Update lastRowOfFirstTable to account for the new data + lastRowOfFirstTable += firstTableData.length; + // Now insert the text that goes between the two tables + + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + + // Start with a base width for each column (optional, but can help with columns that have no data) + // Check if worksheet['!ref'] is defined to prevent errors + const maxCol = worksheet['!ref'] ? worksheet['!ref'].split(':')[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1 : 0; + for (let col = 0; col < maxCol; col++) { + colWidths[col] = 10; // Default base width + } + + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + + // Skip the first row in the jsonData + for (let row = 1; row < jsonData.length; row++) { + jsonData[row].forEach((cell, index) => { + // Only process if the cell is not null/undefined + if (cell) { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + } + }); + } + + // Check if worksheet exists before setting '!cols' + if (worksheet) { + worksheet['!cols'] = colWidths.map((width) => ({ wch: width + 2 })); // +2 for a little extra padding + } + + // Format filename with date + const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD + const filename = `EX01_Financial_Status_Report_${today}.xlsx`; // Append formatted date to the filename + + // Convert workbook back to XLSX file + XLSX.writeFile(workbook, filename); + } else { + throw new Error('Failed to load file'); + } + }; + reader.readAsArrayBuffer(data); + } catch (error) { + console.error('Error downloading the file: ', error); + } + + //setIsLoading(false); + }; + return ( + + + {t("Search Criteria")} + + {criteria.map((c) => { + return ( + + {c.type === "text" && ( + + )} + {c.type === "select" && ( + + {c.label} + + + )} + {c.type === "dateRange" && ( + + + + + + + {"-"} + + + + + + + )} + + ); + })} + + + + + + + + ); +} + +export default SearchBox; diff --git a/src/components/Report/ReportSearchBoxe1/index.ts b/src/components/Report/ReportSearchBoxe1/index.ts new file mode 100644 index 0000000..8f4d421 --- /dev/null +++ b/src/components/Report/ReportSearchBoxe1/index.ts @@ -0,0 +1,3 @@ +//src\components\SearchBox\index.ts +export { default } from "./SearchBoxe1"; +export type { Criterion } from "./SearchBoxe1"; diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 26914fe..5ea1690 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -36,6 +36,7 @@ interface TextCriterion extends BaseCriterion { interface SelectCriterion extends BaseCriterion { type: "select"; options: string[]; + needAll?: boolean; } interface DateRangeCriterion extends BaseCriterion { @@ -134,7 +135,7 @@ function SearchBox({ onChange={makeSelectChangeHandler(c.paramName)} value={inputs[c.paramName]} > - {t("All")} + {!(c.needAll === false) && {t("All")}} {c.options.map((option, index) => ( {t(option)} diff --git a/src/components/SkillSearch/SkillSearch.tsx b/src/components/SkillSearch/SkillSearch.tsx new file mode 100644 index 0000000..01db336 --- /dev/null +++ b/src/components/SkillSearch/SkillSearch.tsx @@ -0,0 +1,96 @@ +"use client"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../SearchBox/index"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults/index"; +import EditNote from "@mui/icons-material/EditNote"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { SkillResult } from "@/app/api/skill"; + +interface Props { + skill: SkillResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const SkillSearch: React.FC = ({ skill }) => { + const { t } = useTranslation(); + const [filteredStaff, setFilteredStaff] = useState(skill); + const router = useRouter(); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Staff Name"), + paramName: "name", + type: "text", + }, + ], + [t] + ); + + const onSkillClick = useCallback( + (skill: SkillResult) => { + console.log(skill); + const id = skill.id; + // router.push(`/settings/skill/edit?id=${id}`); + }, + [router, t] + ); + + const deleteClick = useCallback((skill: SkillResult) => { + // deleteDialog(async () => { + // await deleteStaff(skill.id); + // successDialog("Delete Success", t); + // setFilteredStaff((prev) => prev.filter((obj) => obj.id !== skill.id)); + // }, t); + }, []); + + const columns = useMemo[]>( + () => [ + { + name: "action", + label: t("Actions"), + onClick: onSkillClick, + buttonIcon: , + }, + { name: "name", label: t("Name") }, + { name: "code", label: t("Code") }, + { name: "description", label: t("Description") }, + { + name: "action", + label: t("Actions"), + onClick: deleteClick, + buttonIcon: , + color: "error", + }, + ], + [t, onSkillClick, deleteClick] + ); + + return ( + <> + { + // setFilteredStaff( + // skill.filter( + // (s) => + // s.skillId.toLowerCase().includes(query.skillId.toLowerCase()) && + // s.name.toLowerCase().includes(query.name.toLowerCase()) + // // (query.team === "All" || s.team === query.team) && + // // (query.category === "All" || s.category === query.category) && + // // (query.team === "All" || s.team === query.team), + // ) + // ); + }} + /> + items={filteredStaff} columns={columns} /> + + ); +}; + +export default SkillSearch; diff --git a/src/components/SkillSearch/SkillSearchLoading.tsx b/src/components/SkillSearch/SkillSearchLoading.tsx new file mode 100644 index 0000000..a5959e9 --- /dev/null +++ b/src/components/SkillSearch/SkillSearchLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const SkillSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default SkillSearchLoading; diff --git a/src/components/SkillSearch/SkillSearchWrapper.tsx b/src/components/SkillSearch/SkillSearchWrapper.tsx new file mode 100644 index 0000000..33d0547 --- /dev/null +++ b/src/components/SkillSearch/SkillSearchWrapper.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import SkillSearch from "./SkillSearch"; +import SkillSearchLoading from "./SkillSearchLoading"; +import { comboProp, fetchCompanyCombo } from "@/app/api/companys/actions"; +import { fetchTeamCombo } from "@/app/api/team/actions"; +import { fetchDepartmentCombo } from "@/app/api/departments/actions"; +import { fetchPositionCombo } from "@/app/api/positions/actions"; +import { fetchGradeCombo } from "@/app/api/grades/actions"; +import { fetchSkillCombo } from "@/app/api/skill/actions"; +import { fetchSalaryCombo } from "@/app/api/salarys/actions"; +import { SkillResult, fetchSkill } from "@/app/api/skill"; +// import { preloadStaff } from "@/app/api/staff"; + +interface SubComponents { + Loading: typeof SkillSearchLoading; +} + +const SkillSearchWrapper: React.FC & SubComponents = async () => { + const skill = await fetchSkill() + console.log(skill); + + return ; +}; + +SkillSearchWrapper.Loading = SkillSearchLoading; + +export default SkillSearchWrapper; diff --git a/src/components/SkillSearch/index.ts b/src/components/SkillSearch/index.ts new file mode 100644 index 0000000..5833a58 --- /dev/null +++ b/src/components/SkillSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./SkillSearchWrapper"; diff --git a/src/components/StaffSearch/ConfirmDeleteModal.tsx b/src/components/StaffSearch/ConfirmDeleteModal.tsx deleted file mode 100644 index abeb962..0000000 --- a/src/components/StaffSearch/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client"; -import React, { useCallback, useMemo, useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal, Stack, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { Add } from "@mui/icons-material"; -import Check from "@mui/icons-material/Check"; -import Close from "@mui/icons-material/Close"; -import { TSMS_BUTTON_THEME } from "@/theme/colorConst"; -import { ThemeProvider } from "@emotion/react"; - -interface Props { - isOpen: boolean; - onConfirm: (data: any) => void; - onCancel: (data: any | null) => void; - // staff: StaffResult[]; -} - -const ConfirmModal: React.FC = ({ ...props }) => { - const { t } = useTranslation(); - return ( - <> - - - <> - - {t("Confirm")} - - <> - - {t("Are You Sure")} - - - {/* */} - - - - - {/* */} - - - - - ); -}; - -export default ConfirmModal; diff --git a/src/components/StaffSearch/StaffSearch.tsx b/src/components/StaffSearch/StaffSearch.tsx index e65cfe7..4111d14 100644 --- a/src/components/StaffSearch/StaffSearch.tsx +++ b/src/components/StaffSearch/StaffSearch.tsx @@ -5,15 +5,11 @@ import SearchBox, { Criterion } from "../SearchBox/index"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults/index"; import EditNote from "@mui/icons-material/EditNote"; -import DeleteIcon from '@mui/icons-material/Delete'; -import ConfirmModal from "./ConfirmDeleteModal"; +import DeleteIcon from "@mui/icons-material/Delete"; import { deleteStaff } from "@/app/api/staff/actions"; import { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; -interface combo { - id: any; - label: string; -} interface Props { staff: StaffResult[]; } @@ -24,8 +20,6 @@ type SearchParamNames = keyof SearchQuery; const StaffSearch: React.FC = ({ staff }) => { const { t } = useTranslation(); const [filteredStaff, setFilteredStaff] = useState(staff); - const [data, setData] = useState(); - const [isOpen, setIsOpen] = useState(false); const router = useRouter(); const searchCriteria: Criterion[] = useMemo( @@ -41,10 +35,10 @@ const StaffSearch: React.FC = ({ staff }) => { paramName: "name", type: "text", }, - { - label: t("Staff ID"), - paramName: "staffId", - type: "text" + { + label: t("Staff ID"), + paramName: "staffId", + type: "text", }, { label: t("Grade"), @@ -59,39 +53,26 @@ const StaffSearch: React.FC = ({ staff }) => { options: ["pos1", "CEO"], }, ], - [t], + [t] ); - const onStaffClick = useCallback((staff: StaffResult) => { - console.log(staff); - const id = staff.id - router.push(`/settings/staff/edit?id=${id}`); - }, [router, t]); - - const deleteClick = (staff: StaffResult) => { - console.log(staff); - setData(staff) - setIsOpen(!isOpen) - }; - - const onConfirm = useCallback(async (staff: StaffResult) => { - console.log(staff); - if (data) - await deleteStaff(data) - setIsOpen(false) - window.location.reload; - }, [deleteStaff, data]); + const onStaffClick = useCallback( + (staff: StaffResult) => { + console.log(staff); + const id = staff.id; + router.push(`/settings/staff/edit?id=${id}`); + }, + [router, t] + ); - const onCancel = useCallback((staff: StaffResult) => { - console.log(staff); - setIsOpen(false) + const deleteClick = useCallback((staff: StaffResult) => { + deleteDialog(async () => { + await deleteStaff(staff.id); + successDialog(t("Delete Success"), t); + setFilteredStaff((prev) => prev.filter((obj) => obj.id !== staff.id)); + }, t); }, []); - // useEffect(() => { - // console.log("id"); - // console.log(id); - // }, [id]); - const columns = useMemo[]>( () => [ { @@ -110,34 +91,30 @@ const StaffSearch: React.FC = ({ staff }) => { label: t("Actions"), onClick: deleteClick, buttonIcon: , + color: "error", }, ], - [t, onStaffClick, deleteClick], + [t, onStaffClick, deleteClick] ); return ( <> { + onSearch={(query) => { setFilteredStaff( staff.filter( - (s) => - s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && - s.name.toLowerCase().includes(query.name.toLowerCase()) + (s) => + s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && + s.name.toLowerCase().includes(query.name.toLowerCase()) // (query.team === "All" || s.team === query.team) && // (query.category === "All" || s.category === query.category) && // (query.team === "All" || s.team === query.team), ) - ) + ); }} /> items={filteredStaff} columns={columns} /> - ); }; diff --git a/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx b/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx index 1a9ced0..c335042 100644 --- a/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx +++ b/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx @@ -1,7 +1,7 @@ import { fetchAllCustomers, fetchSubsidiaryTypes } from "@/app/api/subsidiary"; import SubsidiaryDetail from "./SubsidiaryDetail"; -const CustomerDetailWrapper: React.FC = async () => { +const CustomerSaveWrapper: React.FC = async () => { const [customers, subsidiaryTypes] = await Promise.all([ fetchAllCustomers(), @@ -13,4 +13,4 @@ const CustomerDetailWrapper: React.FC = async () => { ); }; -export default CustomerDetailWrapper; +export default CustomerSaveWrapper; diff --git a/src/components/SubsidiarySearch/SubsidiarySearch.tsx b/src/components/SubsidiarySearch/SubsidiarySearch.tsx index 95c901f..c4e1db5 100644 --- a/src/components/SubsidiarySearch/SubsidiarySearch.tsx +++ b/src/components/SubsidiarySearch/SubsidiarySearch.tsx @@ -46,7 +46,7 @@ const SubsidiarySearch: React.FC = ({ subsidiaries }) => { deleteDialog(async() => { await deleteSubsidiary(subsidiary.id) - successDialog("Delete Success", t) + successDialog(t("Delete Success"), t) setFilteredSubsidiaries((prev) => prev.filter((obj) => obj.id !== subsidiary.id)) }, t) diff --git a/src/components/Swal/CustomAlerts.js b/src/components/Swal/CustomAlerts.js index 6eddfb2..668502c 100644 --- a/src/components/Swal/CustomAlerts.js +++ b/src/components/Swal/CustomAlerts.js @@ -50,13 +50,13 @@ export const warningDialog = (text, t) => { }) } -export const submitDialog = async (confirmAction, t) => { +export const submitDialog = async (confirmAction, t, {...props}) => { // const { t } = useTranslation("common") const result = await Swal.fire({ icon: "question", - title: t("Do you want to submit?"), + title: props.title ?? t("Do you want to submit?"), cancelButtonText: t("Cancel"), - confirmButtonText: t("Submit"), + confirmButtonText: props.confirmButtonText ?? t("Submit"), showCancelButton: true, showConfirmButton: true, }); diff --git a/src/components/TableCellEdit/TableCellEdit.tsx b/src/components/TableCellEdit/TableCellEdit.tsx index a96f0b7..0a366fc 100644 --- a/src/components/TableCellEdit/TableCellEdit.tsx +++ b/src/components/TableCellEdit/TableCellEdit.tsx @@ -9,9 +9,9 @@ import { Box, Input, SxProps, TableCell } from "@mui/material"; interface Props { value: T; - onChange: (newValue?: T) => void; + onChange: (newValue: T) => void; renderValue?: (value: T) => string; - convertValue: (inputValue?: string) => T; + convertValue: (inputValue: string) => T; cellSx?: SxProps; inputSx?: SxProps; } @@ -25,7 +25,7 @@ const TableCellEdit = ({ inputSx, }: Props) => { const [editMode, setEditMode] = useState(false); - const [input, setInput] = useState(); + const [input, setInput] = useState(""); const inputRef = useRef(null); const onClick = useCallback(() => { @@ -41,7 +41,7 @@ const TableCellEdit = ({ const onBlur = useCallback(() => { setEditMode(false); onChange(convertValue(input)); - setInput(undefined); + setInput(""); }, [convertValue, input, onChange]); useEffect(() => { diff --git a/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx b/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx index 72be3c1..7563f53 100644 --- a/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx +++ b/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx @@ -6,6 +6,10 @@ import SearchBox, { Criterion } from "../SearchBox"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import EditNote from "@mui/icons-material/EditNote"; +import { useRouter, useSearchParams } from "next/navigation"; +import DeleteIcon from '@mui/icons-material/Delete'; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { deleteTaskTemplate } from "@/app/api/tasks/actions"; interface Props { taskTemplates: TaskTemplate[]; @@ -16,6 +20,8 @@ type SearchParamNames = keyof SearchQuery; const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { const { t } = useTranslation("tasks"); + const searchParams = useSearchParams() + const router = useRouter() const [filteredTemplates, setFilteredTemplates] = useState(taskTemplates); const searchCriteria: Criterion[] = useMemo( @@ -30,7 +36,20 @@ const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { }, [taskTemplates]); const onTaskClick = useCallback((taskTemplate: TaskTemplate) => { - console.log(taskTemplate); + const params = new URLSearchParams(searchParams.toString()) + params.set("id", taskTemplate.id.toString()) + router.replace(`/tasks/edit?${params.toString()}`); + }, []); + + const onDeleteClick = useCallback((taskTemplate: TaskTemplate) => { + + deleteDialog(async () => { + await deleteTaskTemplate(taskTemplate.id) + + successDialog(t("Delete Success"), t) + + setFilteredTemplates((prev) => prev.filter((obj) => obj.id !== taskTemplate.id)) + }, t) }, []); const columns = useMemo[]>( @@ -43,6 +62,13 @@ const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { }, { name: "code", label: t("Task Template Code") }, { name: "name", label: t("Task Template Name") }, + { + name: "id", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + color: "error" + }, ], [onTaskClick, t], ); diff --git a/src/components/TeamSearch/ConfirmDeleteModal.tsx b/src/components/TeamSearch/ConfirmDeleteModal.tsx deleted file mode 100644 index a5e7ed0..0000000 --- a/src/components/TeamSearch/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; -import React, { useCallback, useMemo, useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal, Stack, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { Add } from "@mui/icons-material"; -import Check from "@mui/icons-material/Check"; -import Close from "@mui/icons-material/Close"; -import { TSMS_BUTTON_THEME } from "@/theme/colorConst"; -import { ThemeProvider } from "@emotion/react"; - -interface Props { - isOpen: boolean; - onConfirm: (data: any) => void; - onCancel: (data: any | null) => void; -} - -const ConfirmModal: React.FC = ({ ...props }) => { - const { t } = useTranslation(); - return ( - <> - - - <> - - {t("Confirm")} - - <> - - {t("Are You Sure")} - - - {/* */} - - - - - {/* */} - - - - - ); -}; - -export default ConfirmModal; diff --git a/src/components/TeamSearch/TeamSearch.tsx b/src/components/TeamSearch/TeamSearch.tsx index b2cc9e8..71ecb79 100644 --- a/src/components/TeamSearch/TeamSearch.tsx +++ b/src/components/TeamSearch/TeamSearch.tsx @@ -6,12 +6,10 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults/index"; import EditNote from "@mui/icons-material/EditNote"; -import DeleteIcon from '@mui/icons-material/Delete'; -import { deleteStaff } from "@/app/api/staff/actions"; +import DeleteIcon from "@mui/icons-material/Delete"; import { useRouter } from "next/navigation"; -import ConfirmModal from "./ConfirmDeleteModal"; import { deleteTeam } from "@/app/api/team/actions"; - +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; interface Props { team: TeamResult[]; @@ -20,109 +18,90 @@ type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; const TeamSearch: React.FC = ({ team }) => { - const { t } = useTranslation(); - const [filteredTeam, setFilteredTeam] = useState(team); - const [data, setData] = useState(); - const [isOpen, setIsOpen] = useState(false); - const router = useRouter(); - - const searchCriteria: Criterion[] = useMemo( - () => [ - { - label: t("Team Name"), - paramName: "name", - type: "text", - }, - { - label: t("Team Code"), - paramName: "code", - type: "text", - }, - { - label: t("Team Description"), - paramName: "description", - type: "text", - }, - ], - [t], - ); + const { t } = useTranslation(); + const [filteredTeam, setFilteredTeam] = useState(team); + const router = useRouter(); - const onTeamClick = useCallback((team: TeamResult) => { - console.log(team); - const id = team.id - router.push(`/settings/team/edit?id=${id}`); - }, [router, t]); - - // const onDeleteClick = useCallback((team: TeamResult) => { - // console.log(team); - // deleteTeam + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Team Name"), + paramName: "name", + type: "text", + }, + { + label: t("Team Code"), + paramName: "code", + type: "text", + }, + { + label: t("Team Description"), + paramName: "description", + type: "text", + }, + ], + [t] + ); - // }, [router, t]); + const onTeamClick = useCallback( + (team: TeamResult) => { + console.log(team); + const id = team.id; + router.push(`/settings/team/edit?id=${id}`); + }, + [router, t] + ); - const onDeleteClick = (team: TeamResult) => { - console.log(team); - setData(team) - setIsOpen(!isOpen) - }; + const onDeleteClick = useCallback((team: TeamResult) => { + deleteDialog(async () => { + await deleteTeam(team.id); - const onConfirm = useCallback(async (team: TeamResult) => { - console.log(team); - if (data) - await deleteTeam(data) - setIsOpen(false) - window.location.reload; - }, [deleteTeam, data]); + successDialog(t("Delete Success"), t); - const onCancel = useCallback(() => { - setIsOpen(false) - }, []); + setFilteredTeam((prev) => prev.filter((obj) => obj.id !== team.id)); + }, t); + }, []); - const columns = useMemo[]>( - () => [ - { - name: "action", - label: t("Edit"), - onClick: onTeamClick, - buttonIcon: , - }, - { name: "name", label: t("Name") }, - { name: "code", label: t("Code") }, - { name: "description", label: t("description") }, - { - name: "action", - label: t("Delete"), - onClick: onDeleteClick, - buttonIcon: , - }, - ], - [t], - ); + const columns = useMemo[]>( + () => [ + { + name: "action", + label: t("Edit"), + onClick: onTeamClick, + buttonIcon: , + }, + { name: "name", label: t("Name") }, + { name: "code", label: t("Code") }, + { name: "description", label: t("description") }, + { name: "staffName", label: t("TeamLead") }, + { + name: "action", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + color: "error" + }, + ], + [t] + ); return ( - <> - + { - // setFilteredStaff( - // staff.filter( - // (s) => - // s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && - // s.name.toLowerCase().includes(query.name.toLowerCase()) - // // (query.team === "All" || s.team === query.team) && - // // (query.category === "All" || s.category === query.category) && - // // (query.team === "All" || s.team === query.team), - // ) - // ) + onSearch={(query) => { + setFilteredTeam( + team.filter( + (t) => + t.name.toLowerCase().includes(query.name.toLowerCase()) && + t.code.toLowerCase().includes(query.code.toLowerCase()) && + t.description.toLowerCase().includes(query.description.toLowerCase()) + ) + ) }} /> items={filteredTeam} columns={columns} /> - - - + ); }; export default TeamSearch; diff --git a/src/components/TimesheetModal/TimesheetModal.tsx b/src/components/TimesheetModal/TimesheetModal.tsx index 055e0d9..d6146b0 100644 --- a/src/components/TimesheetModal/TimesheetModal.tsx +++ b/src/components/TimesheetModal/TimesheetModal.tsx @@ -16,11 +16,13 @@ import { FormProvider, useForm } from "react-hook-form"; import { RecordTimesheetInput } from "@/app/api/timesheets/actions"; import dayjs from "dayjs"; import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; +import { AssignedProject } from "@/app/api/projects"; interface Props { isOpen: boolean; onClose: () => void; timesheetType: "time" | "leave"; + assignedProjects: AssignedProject[]; } const modalSx: SxProps = { @@ -37,6 +39,7 @@ const TimesheetModal: React.FC = ({ isOpen, onClose, timesheetType, + assignedProjects, }) => { const { t } = useTranslation("home"); @@ -73,7 +76,7 @@ const TimesheetModal: React.FC = ({ marginBlock: 4, }} > - + - - + {Boolean(assignedProjects.length) && ( + + + + + )} - - - + {assignedProjects.length > 0 ? ( + <> + + + + + ) : ( + <> + + {t("You have no assigned projects!")} + + + )} ); }; diff --git a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx index 42b3756..c311488 100644 --- a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx +++ b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx @@ -1,65 +1,9 @@ +import { fetchAssignedProjects } from "@/app/api/projects"; import UserWorkspacePage from "./UserWorkspacePage"; -export interface ProjectHours { - code: string; - name: string; - hoursSpent: number; - hoursSpentOther: number; - hoursAllocated: number; - hoursAllocatedOther: number; - projectStatus: "On Track" | "Potential Delay"; -} - -const mockProjectCards: ProjectHours[] = [ - { - code: "M1001 (C)", - name: "Consultancy Project A", - hoursSpent: 12.75, - hoursSpentOther: 0.0, - hoursAllocated: 150.0, - hoursAllocatedOther: 30.0, - projectStatus: "On Track", - }, - { - code: "M1301 (C)", - name: "Consultancy Project AAA", - hoursSpent: 4.25, - hoursSpentOther: 0.25, - hoursAllocated: 30.0, - hoursAllocatedOther: 0.0, - projectStatus: "On Track", - }, - { - code: "M1354 (C)", - name: "Consultancy Project BBB", - hoursSpent: 57.0, - hoursSpentOther: 6.5, - hoursAllocated: 100.0, - hoursAllocatedOther: 20.0, - projectStatus: "On Track", - }, - { - code: "M1973 (C)", - name: "Construction Project CCC", - hoursSpent: 12.75, - hoursSpentOther: 0.0, - hoursAllocated: 150.0, - hoursAllocatedOther: 30.0, - projectStatus: "Potential Delay", - }, - { - code: "M2014 (T)", - name: "Consultancy Project DDD", - hoursSpent: 1.0, - hoursSpentOther: 0.0, - hoursAllocated: 10.0, - hoursAllocatedOther: 0.0, - projectStatus: "Potential Delay", - }, -]; - -const UserWorkspaceWrapper: React.FC = () => { - return ; +const UserWorkspaceWrapper: React.FC = async () => { + const assignedProjects = await fetchAssignedProjects(); + return ; }; export default UserWorkspaceWrapper; diff --git a/src/i18n/en/claim.json b/src/i18n/en/claim.json index 51f657c..b5b5e42 100644 --- a/src/i18n/en/claim.json +++ b/src/i18n/en/claim.json @@ -31,6 +31,7 @@ "Please ensure the projects are selected": "Please ensure the projects are selected", "Please ensure the amount are correct": "Please ensure the amount are correct", + "Details": "Details", "Description": "Description", "Actions": "Actions" } \ No newline at end of file diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 5f1d289..a7d019a 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -17,6 +17,8 @@ "Do you want to delete?": "Do you want to delete", "Delete Success": "Delete Success", + "Details": "Details", + "Delete": "Delete", "Search": "Search", "Search Criteria": "Search Criteria", "Cancel": "Cancel", diff --git a/src/i18n/en/report.json b/src/i18n/en/report.json new file mode 100644 index 0000000..e7e61fb --- /dev/null +++ b/src/i18n/en/report.json @@ -0,0 +1,3 @@ +{ + "Project": "Project" +} \ No newline at end of file diff --git a/src/i18n/en/tasks.json b/src/i18n/en/tasks.json new file mode 100644 index 0000000..d70d00a --- /dev/null +++ b/src/i18n/en/tasks.json @@ -0,0 +1,27 @@ +{ + "Task Template": "Task Template", + "Create Task Template": "Create Task Template", + "Edit Task Template": "Edit Task Template", + + "Task Template Code": "Task Template Code", + "Task Template Name": "Task Template Name", + "Task List Setup": "Task List Setup", + "Task Pool": "Task Pool", + "Task List Template": "Task List Template", + + "Task template code is required": "Task template code is required", + "Task template name is required": "Task template name is required", + + "Do you want to submit?": "Do you want to submit?", + "Submit Success": "Submit Success", + "Submit Fail": "Submit Fail", + "Do you want to delete?": "Do you want to delete?", + "Delete Success": "Delete Success", + + "selected": "selected", + "Details": "Details", + "Delete": "Delete", + "Cancel": "Cancel", + "Submit": "Submit", + "Confirm": "Confirm" +} \ No newline at end of file diff --git a/src/i18n/zh/claim.json b/src/i18n/zh/claim.json index 92e5b7e..9e2de7e 100644 --- a/src/i18n/zh/claim.json +++ b/src/i18n/zh/claim.json @@ -31,6 +31,7 @@ "Please ensure the projects are selected": "請確保所有項目欄位已選擇", "Please ensure the amount are correct": "請確保所有金額輸入正確", + "Details": "詳請", "Description": "描述", "Actions": "行動" } \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index e4642ea..4ff2fcf 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -15,6 +15,8 @@ "Do you want to delete?": "你是否確認要刪除?", "Delete Success": "刪除成功", + "Details": "詳情", + "Delete": "刪除", "Search": "搜尋", "Search Criteria": "搜尋條件", "Cancel": "取消", diff --git a/src/i18n/zh/report.json b/src/i18n/zh/report.json new file mode 100644 index 0000000..a6257cf --- /dev/null +++ b/src/i18n/zh/report.json @@ -0,0 +1,3 @@ +{ + "Project": "項目" +} \ No newline at end of file diff --git a/src/i18n/zh/tasks.json b/src/i18n/zh/tasks.json new file mode 100644 index 0000000..16ba727 --- /dev/null +++ b/src/i18n/zh/tasks.json @@ -0,0 +1,27 @@ +{ + "Task Template": "工作範本", + "Create Task Template": "建立工作範本", + "Edit Task Template": "編輯工作範本", + + "Task Template Code": "工作範本編號", + "Task Template Name": "工作範本名稱", + "Task List Setup": "工作名單設置", + "Task Pool": "所有工作", + "Task List Template": "工作名單範本", + + "Task template code is required": "需要工作範本編號", + "Task template name is required": "需要工作範本名稱", + + "Do you want to submit?": "你是否確認要提交?", + "Submit Success": "提交成功", + "Submit Fail": "提交失敗", + "Do you want to delete?": "你是否確認要刪除?", + "Delete Success": "刪除成功", + + "selected": "已選擇", + "Details": "詳情", + "Delete": "刪除", + "Cancel": "取消", + "Submit": "提交", + "Confirm": "確認" +} \ No newline at end of file