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