| @@ -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", | |||
| @@ -37,7 +38,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 +1935,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 +3547,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", | |||
| @@ -4150,6 +4182,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 +4288,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", | |||
| @@ -4316,6 +4388,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", | |||
| @@ -5431,6 +5514,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 +5567,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", | |||
| @@ -5599,6 +5695,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", | |||
| @@ -7880,6 +7984,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", | |||
| @@ -8631,6 +8746,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 +9979,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 +10380,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", | |||
| @@ -59,6 +59,11 @@ export interface AssignedProject { | |||
| endDate: string; | |||
| }; | |||
| }; | |||
| // Manhour info | |||
| hoursSpent: number; | |||
| hoursSpentOther: number; | |||
| hoursAllocated: number; | |||
| hoursAllocatedOther: number; | |||
| } | |||
| export const preloadProjects = () => { | |||
| @@ -131,3 +136,12 @@ export const fetchProjectWorkNatures = cache(async () => { | |||
| next: { tags: ["projectWorkNatures"] }, | |||
| }); | |||
| }); | |||
| export const fetchAssignedProjects = cache(async () => { | |||
| return serverFetchJson<AssignedProject[]>( | |||
| `${BASE_API_URL}/projects/assignedProjects`, | |||
| { | |||
| next: { tags: ["assignedProjects"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| @@ -8,16 +8,17 @@ export const serverFetch: typeof fetch = async (input, init) => { | |||
| const session = await getServerSession<any, SessionWithTokens>(authOptions); | |||
| const accessToken = session?.accessToken; | |||
| console.log(accessToken) | |||
| console.log(accessToken); | |||
| return fetch(input, { | |||
| ...init, | |||
| headers: { | |||
| ...init?.headers, | |||
| ...(accessToken | |||
| ? { | |||
| Authorization: `Bearer ${accessToken}`, | |||
| Accept: "application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" | |||
| } | |||
| Authorization: `Bearer ${accessToken}`, | |||
| Accept: | |||
| "application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |||
| } | |||
| : {}), | |||
| }, | |||
| }); | |||
| @@ -61,27 +62,28 @@ export async function serverFetchBlob<T>(...args: FetchParams) { | |||
| const response = await serverFetch(...args); | |||
| if (response.ok) { | |||
| const body = response.body | |||
| const body = response.body; | |||
| // console.log(body) | |||
| // console.log(body?.tee()[0].getReader()) | |||
| const reader = body?.getReader() | |||
| let finalUInt8Array = new Uint8Array() | |||
| let done = false | |||
| const reader = body?.getReader(); | |||
| let finalUInt8Array = new Uint8Array(); | |||
| let done = false; | |||
| while (!done) { | |||
| const read = await reader?.read() | |||
| const read = await reader?.read(); | |||
| // version 1 | |||
| if (read?.done) { | |||
| done = true | |||
| 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) | |||
| 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) | |||
| } | |||
| @@ -115,7 +117,10 @@ export async function serverFetchBlob<T>(...args: FetchParams) { | |||
| // console.log("2", finalUInt8Array) | |||
| // console.log(bodyValue) | |||
| return { filename: response.headers.get("filename"), blobValue: finalUInt8Array } as T; | |||
| return { | |||
| filename: response.headers.get("filename"), | |||
| blobValue: finalUInt8Array, | |||
| } as T; | |||
| } else { | |||
| switch (response.status) { | |||
| case 401: | |||
| @@ -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<Props> = ({ | |||
| isOpen, | |||
| onClose, | |||
| timesheetType, | |||
| assignedProjects, | |||
| }) => { | |||
| const { t } = useTranslation("home"); | |||
| @@ -73,7 +76,7 @@ const TimesheetModal: React.FC<Props> = ({ | |||
| marginBlock: 4, | |||
| }} | |||
| > | |||
| <TimesheetTable /> | |||
| <TimesheetTable assignedProjects={assignedProjects} /> | |||
| </Box> | |||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||
| <Button | |||
| @@ -26,113 +26,10 @@ import isBetween from "dayjs/plugin/isBetween"; | |||
| dayjs.extend(isBetween); | |||
| const mockProjects: AssignedProject[] = [ | |||
| { | |||
| id: 1, | |||
| name: "Consultancy Project A", | |||
| code: "M1001 (C)", | |||
| tasks: [ | |||
| { | |||
| id: 1, | |||
| name: "1.1 Preparation of preliminary Cost Estimate / Cost Plan including Revised & Refined", | |||
| description: null, | |||
| taskGroup: { | |||
| id: 1, | |||
| name: "1. Design & Cost Planning / Estimating", | |||
| }, | |||
| }, | |||
| { | |||
| id: 6, | |||
| name: "2.1 Advise on tendering & contractual arrangement", | |||
| description: null, | |||
| taskGroup: { | |||
| id: 2, | |||
| name: "2. Tender Documentation", | |||
| }, | |||
| }, | |||
| ], | |||
| milestones: { | |||
| 1: { | |||
| startDate: "2000-01-01", | |||
| endDate: "2100-01-01", | |||
| }, | |||
| 2: { | |||
| startDate: "2100-01-01", | |||
| endDate: "2100-01-02", | |||
| }, | |||
| }, | |||
| }, | |||
| { | |||
| id: 2, | |||
| name: "Consultancy Project B", | |||
| code: "M1354 (C)", | |||
| tasks: [ | |||
| { | |||
| id: 1, | |||
| name: "1.1 Preparation of preliminary Cost Estimate / Cost Plan including Revised & Refined", | |||
| description: null, | |||
| taskGroup: { | |||
| id: 1, | |||
| name: "1. Design & Cost Planning / Estimating", | |||
| }, | |||
| }, | |||
| { | |||
| id: 10, | |||
| name: "3.5 Attend tender interviews", | |||
| description: null, | |||
| taskGroup: { | |||
| id: 3, | |||
| name: "3. Tender Analysis & Report & Contract Documentation", | |||
| }, | |||
| }, | |||
| ], | |||
| milestones: { | |||
| 1: { | |||
| startDate: "2000-01-01", | |||
| endDate: "2100-01-01", | |||
| }, | |||
| 3: { | |||
| startDate: "2100-01-01", | |||
| endDate: "2100-01-02", | |||
| }, | |||
| }, | |||
| }, | |||
| { | |||
| id: 3, | |||
| name: "Consultancy Project C", | |||
| code: "M1973 (C)", | |||
| tasks: [ | |||
| { | |||
| id: 1, | |||
| name: "1.1 Preparation of preliminary Cost Estimate / Cost Plan including Revised & Refined", | |||
| description: null, | |||
| taskGroup: { | |||
| id: 1, | |||
| name: "1. Design & Cost Planning / Estimating", | |||
| }, | |||
| }, | |||
| { | |||
| id: 20, | |||
| name: "4.10 Preparation of Statement of Final Account", | |||
| description: null, | |||
| taskGroup: { | |||
| id: 4, | |||
| name: "4. Construction / Post Construction", | |||
| }, | |||
| }, | |||
| ], | |||
| milestones: { | |||
| 1: { | |||
| startDate: "2000-01-01", | |||
| endDate: "2100-01-01", | |||
| }, | |||
| 4: { | |||
| startDate: "2100-01-01", | |||
| endDate: "2100-01-02", | |||
| }, | |||
| }, | |||
| }, | |||
| ]; | |||
| interface Props { | |||
| day: string; | |||
| assignedProjects: AssignedProject[]; | |||
| } | |||
| type TimeEntryRow = Partial< | |||
| TimeEntry & { | |||
| @@ -144,10 +41,10 @@ type TimeEntryRow = Partial< | |||
| } | |||
| >; | |||
| const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { | |||
| const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => { | |||
| const { t } = useTranslation("home"); | |||
| const taskGroupsByProject = useMemo(() => { | |||
| return mockProjects.reduce<{ | |||
| return assignedProjects.reduce<{ | |||
| [projectId: AssignedProject["id"]]: { | |||
| value: TaskGroup["id"]; | |||
| label: string; | |||
| @@ -164,16 +61,16 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { | |||
| ), | |||
| }; | |||
| }, {}); | |||
| }, []); | |||
| }, [assignedProjects]); | |||
| // To check for start / end planned dates | |||
| const milestonesByProject = useMemo(() => { | |||
| return mockProjects.reduce<{ | |||
| return assignedProjects.reduce<{ | |||
| [projectId: AssignedProject["id"]]: AssignedProject["milestones"]; | |||
| }>((acc, project) => { | |||
| return { ...acc, [project.id]: { ...project.milestones } }; | |||
| }, {}); | |||
| }, []); | |||
| }, [assignedProjects]); | |||
| const { getValues, setValue } = useFormContext<RecordTimesheetInput>(); | |||
| const currentEntries = getValues(day); | |||
| @@ -322,7 +219,7 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { | |||
| editable: true, | |||
| type: "singleSelect", | |||
| valueOptions() { | |||
| return mockProjects.map((p) => ({ value: p.id, label: p.name })); | |||
| return assignedProjects.map((p) => ({ value: p.id, label: p.name })); | |||
| }, | |||
| }, | |||
| { | |||
| @@ -339,7 +236,7 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { | |||
| return []; | |||
| } | |||
| const projectInfo = mockProjects.find( | |||
| const projectInfo = assignedProjects.find( | |||
| (p) => p.id === updatedRow.projectId, | |||
| ); | |||
| @@ -364,7 +261,7 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { | |||
| return []; | |||
| } | |||
| const projectInfo = mockProjects.find( | |||
| const projectInfo = assignedProjects.find( | |||
| (p) => p.id === updatedRow.projectId, | |||
| ); | |||
| @@ -399,6 +296,7 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { | |||
| handleCancel, | |||
| apiRef, | |||
| taskGroupsByProject, | |||
| assignedProjects, | |||
| ], | |||
| ); | |||
| @@ -18,8 +18,13 @@ import React, { useState } from "react"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import EntryInputTable from "./EntryInputTable"; | |||
| import { AssignedProject } from "@/app/api/projects"; | |||
| const TimesheetTable: React.FC = () => { | |||
| interface Props { | |||
| assignedProjects: AssignedProject[]; | |||
| } | |||
| const TimesheetTable: React.FC<Props> = ({ assignedProjects }) => { | |||
| const { t } = useTranslation("home"); | |||
| const { watch } = useFormContext<RecordTimesheetInput>(); | |||
| @@ -40,7 +45,12 @@ const TimesheetTable: React.FC = () => { | |||
| {days.map((day, index) => { | |||
| const entries = currentInput[day]; | |||
| return ( | |||
| <DayRow key={`${day}${index}`} day={day} entries={entries} /> | |||
| <DayRow | |||
| key={`${day}${index}`} | |||
| day={day} | |||
| entries={entries} | |||
| assignedProjects={assignedProjects} | |||
| /> | |||
| ); | |||
| })} | |||
| </TableBody> | |||
| @@ -49,10 +59,11 @@ const TimesheetTable: React.FC = () => { | |||
| ); | |||
| }; | |||
| const DayRow: React.FC<{ day: string; entries: TimeEntry[] }> = ({ | |||
| day, | |||
| entries, | |||
| }) => { | |||
| const DayRow: React.FC<{ | |||
| day: string; | |||
| entries: TimeEntry[]; | |||
| assignedProjects: AssignedProject[]; | |||
| }> = ({ day, entries, assignedProjects }) => { | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| @@ -106,7 +117,7 @@ const DayRow: React.FC<{ day: string; entries: TimeEntry[] }> = ({ | |||
| > | |||
| <Collapse in={open} timeout="auto" unmountOnExit> | |||
| <Box> | |||
| <EntryInputTable day={day} /> | |||
| <EntryInputTable day={day} assignedProjects={assignedProjects} /> | |||
| </Box> | |||
| </Collapse> | |||
| </TableCell> | |||
| @@ -1,15 +1,10 @@ | |||
| import React, { useEffect, useMemo } from "react"; | |||
| import React, { useEffect } from "react"; | |||
| import { | |||
| Card, | |||
| CardContent, | |||
| FormControl, | |||
| Grid, | |||
| IconButton, | |||
| InputAdornment, | |||
| InputLabel, | |||
| MenuItem, | |||
| Select, | |||
| SelectChangeEvent, | |||
| Stack, | |||
| TextField, | |||
| Typography, | |||
| @@ -18,13 +13,15 @@ import { useTranslation } from "react-i18next"; | |||
| import { Clear, Search } from "@mui/icons-material"; | |||
| import ProjectGrid from "./ProjectGrid"; | |||
| import { Props as UserWorkspaceProps } from "./UserWorkspacePage"; | |||
| import uniq from "lodash/uniq"; | |||
| const AssignedProjects: React.FC<UserWorkspaceProps> = ({ allProjects }) => { | |||
| const AssignedProjects: React.FC<UserWorkspaceProps> = ({ | |||
| assignedProjects, | |||
| }) => { | |||
| const { t } = useTranslation("home"); | |||
| // Projects | |||
| const [filteredProjects, setFilterProjects] = React.useState(allProjects); | |||
| const [filteredProjects, setFilterProjects] = | |||
| React.useState(assignedProjects); | |||
| // Query related | |||
| const [query, setQuery] = React.useState(""); | |||
| @@ -37,28 +34,15 @@ const AssignedProjects: React.FC<UserWorkspaceProps> = ({ allProjects }) => { | |||
| setQuery(""); | |||
| }, []); | |||
| // Filter | |||
| const allStatuses = useMemo(() => { | |||
| return uniq([ | |||
| "All", | |||
| ...allProjects.map((project) => project.projectStatus), | |||
| ]); | |||
| }, [allProjects]); | |||
| const [statusFilter, setStatusFilter] = React.useState("All"); | |||
| const onStatusChange = React.useCallback((e: SelectChangeEvent) => { | |||
| setStatusFilter(e.target.value); | |||
| }, []); | |||
| useEffect(() => { | |||
| setFilterProjects( | |||
| allProjects.filter( | |||
| assignedProjects.filter( | |||
| (p) => | |||
| (p.code.toLowerCase().includes(query.toLowerCase()) || | |||
| p.name.toLowerCase().includes(query.toLowerCase())) && | |||
| (p.projectStatus === statusFilter || statusFilter === "All"), | |||
| p.code.toLowerCase().includes(query.toLowerCase()) || | |||
| p.name.toLowerCase().includes(query.toLowerCase()), | |||
| ), | |||
| ); | |||
| }, [allProjects, query, statusFilter]); | |||
| }, [assignedProjects, query]); | |||
| return ( | |||
| <> | |||
| @@ -88,23 +72,6 @@ const AssignedProjects: React.FC<UserWorkspaceProps> = ({ allProjects }) => { | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={3}> | |||
| <FormControl fullWidth> | |||
| <InputLabel size="small">{t("Project Status")}</InputLabel> | |||
| <Select | |||
| label={t("Project Status")} | |||
| size="small" | |||
| value={statusFilter} | |||
| onChange={onStatusChange} | |||
| > | |||
| {allStatuses.map((option, index) => ( | |||
| <MenuItem key={`${option}-${index}`} value={option}> | |||
| {option} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| </Grid> | |||
| </Stack> | |||
| </CardContent> | |||
| @@ -1,11 +1,11 @@ | |||
| import React from "react"; | |||
| import { ProjectHours } from "./UserWorkspaceWrapper"; | |||
| import { Box, Card, CardContent, Chip, Grid, Typography } from "@mui/material"; | |||
| import { Box, Card, CardContent, Grid, Typography } from "@mui/material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { manhourFormatter } from "@/app/utils/formatUtil"; | |||
| import { AssignedProject } from "@/app/api/projects"; | |||
| interface Props { | |||
| projects: ProjectHours[]; | |||
| projects: AssignedProject[]; | |||
| } | |||
| const ProjectGrid: React.FC<Props> = ({ projects }) => { | |||
| @@ -17,23 +17,6 @@ const ProjectGrid: React.FC<Props> = ({ projects }) => { | |||
| <Grid key={`${project.code}${idx}`} item xs={4}> | |||
| <Card> | |||
| <CardContent> | |||
| <Box | |||
| sx={{ | |||
| display: "flex", | |||
| justifyContent: "flex-end", | |||
| marginBlockEnd: 1, | |||
| }} | |||
| > | |||
| <Chip | |||
| size="small" | |||
| label={project.projectStatus} | |||
| color={ | |||
| project.projectStatus === "On Track" | |||
| ? "success" | |||
| : "warning" | |||
| } | |||
| /> | |||
| </Box> | |||
| <Typography variant="overline">{project.code}</Typography> | |||
| <Typography | |||
| variant="h6" | |||
| @@ -8,14 +8,14 @@ import { Add } from "@mui/icons-material"; | |||
| import { Typography } from "@mui/material"; | |||
| import ButtonGroup from "@mui/material/ButtonGroup"; | |||
| import AssignedProjects from "./AssignedProjects"; | |||
| import { ProjectHours } from "./UserWorkspaceWrapper"; | |||
| import TimesheetModal from "../TimesheetModal"; | |||
| import { AssignedProject } from "@/app/api/projects"; | |||
| export interface Props { | |||
| allProjects: ProjectHours[]; | |||
| assignedProjects: AssignedProject[]; | |||
| } | |||
| const UserWorkspacePage: React.FC<Props> = ({ allProjects }) => { | |||
| const UserWorkspacePage: React.FC<Props> = ({ assignedProjects }) => { | |||
| const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false); | |||
| const [isLeaveModalVisible, setLeaveModalVisible] = useState(false); | |||
| const { t } = useTranslation("home"); | |||
| @@ -67,13 +67,15 @@ const UserWorkspacePage: React.FC<Props> = ({ allProjects }) => { | |||
| timesheetType="time" | |||
| isOpen={isTimeheetModalVisible} | |||
| onClose={handleCloseTimesheetModal} | |||
| assignedProjects={assignedProjects} | |||
| /> | |||
| <TimesheetModal | |||
| timesheetType="leave" | |||
| isOpen={isLeaveModalVisible} | |||
| onClose={handleCloseLeaveModal} | |||
| assignedProjects={assignedProjects} | |||
| /> | |||
| <AssignedProjects allProjects={allProjects} /> | |||
| <AssignedProjects assignedProjects={assignedProjects} /> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -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 <UserWorkspacePage allProjects={mockProjectCards} />; | |||
| const UserWorkspaceWrapper: React.FC = async () => { | |||
| const assignedProjects = await fetchAssignedProjects(); | |||
| return <UserWorkspacePage assignedProjects={assignedProjects} />; | |||
| }; | |||
| export default UserWorkspaceWrapper; | |||