| @@ -13,6 +13,7 @@ import { | |||||
| fetchProjectWithTasks, | fetchProjectWithTasks, | ||||
| } from "@/app/api/projects"; | } from "@/app/api/projects"; | ||||
| import { fetchHolidays } from "@/app/api/holidays"; | import { fetchHolidays } from "@/app/api/holidays"; | ||||
| import { fetchAllTasks } from "@/app/api/tasks"; | |||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "User Workspace", | title: "User Workspace", | ||||
| @@ -27,6 +28,7 @@ const Home: React.FC = async () => { | |||||
| fetchHolidays(); | fetchHolidays(); | ||||
| fetchTeamMemberTimesheets(); | fetchTeamMemberTimesheets(); | ||||
| fetchTeamMemberLeaves(); | fetchTeamMemberLeaves(); | ||||
| fetchAllTasks(); | |||||
| return ( | return ( | ||||
| <I18nProvider namespaces={["home", "common"]}> | <I18nProvider namespaces={["home", "common"]}> | ||||
| @@ -51,7 +51,9 @@ export const preloadAllTasks = () => { | |||||
| }; | }; | ||||
| export const fetchAllTasks = cache(async () => { | export const fetchAllTasks = cache(async () => { | ||||
| return serverFetchJson<Task[]>(`${BASE_API_URL}/tasks`); | |||||
| return serverFetchJson<Task[]>(`${BASE_API_URL}/tasks`, { | |||||
| next: { tags: ["tasks"] }, | |||||
| }); | |||||
| }); | }); | ||||
| export const fetchTaskTemplateDetail = cache(async (id: string) => { | export const fetchTaskTemplateDetail = cache(async (id: string) => { | ||||
| @@ -158,3 +158,27 @@ export const importTimesheets = async (data: FormData) => { | |||||
| return importTimesheets; | return importTimesheets; | ||||
| }; | }; | ||||
| export const importLeaves = async (data: FormData) => { | |||||
| const importLeaves = await serverFetchString<string>( | |||||
| `${BASE_API_URL}/timesheets/import-leave`, | |||||
| { | |||||
| method: "POST", | |||||
| body: data, | |||||
| }, | |||||
| ); | |||||
| return importLeaves; | |||||
| }; | |||||
| export const rearrangeTimesheets = async () => { | |||||
| const importLeaves = await serverFetchString<string>( | |||||
| `${BASE_API_URL}/timesheets/rearrange`, | |||||
| { | |||||
| method: "POST", | |||||
| // body: data, | |||||
| }, | |||||
| ); | |||||
| return importLeaves; | |||||
| }; | |||||
| @@ -43,7 +43,9 @@ export const validateTimeEntry = ( | |||||
| error.taskId = "Required"; | error.taskId = "Required"; | ||||
| } | } | ||||
| } else { | } else { | ||||
| if (!entry.remark) { | |||||
| if (entry.taskGroupId && !entry.taskId) { | |||||
| error.taskId = "Required"; | |||||
| } else if (!entry.remark) { | |||||
| error.remark = "Required for non-billable tasks"; | error.remark = "Required for non-billable tasks"; | ||||
| } | } | ||||
| } | } | ||||
| @@ -133,6 +133,7 @@ const CompanyHoliday: React.FC<Props> = ({ holidays, abilities }) => { | |||||
| eventClick={handleEventClick} | eventClick={handleEventClick} | ||||
| headerToolbar={{ | headerToolbar={{ | ||||
| start: "today prev next", | start: "today prev next", | ||||
| center: "title", | |||||
| end: "dayGridMonth listMonth", | end: "dayGridMonth listMonth", | ||||
| }} | }} | ||||
| buttonText={{ | buttonText={{ | ||||
| @@ -5,8 +5,8 @@ import { FileUpload } from "@mui/icons-material"; | |||||
| import { Button, Grid, Stack } from "@mui/material"; | import { Button, Grid, Stack } from "@mui/material"; | ||||
| import React, { ChangeEvent, useCallback } from "react"; | import React, { ChangeEvent, useCallback } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { errorDialogWithContent, successDialog, successDialogWithContent } from "../Swal/CustomAlerts"; | |||||
| import { importTimesheets } from "@/app/api/timesheets/actions"; | |||||
| import { errorDialogWithContent, submitDialog, successDialog, successDialogWithContent } from "../Swal/CustomAlerts"; | |||||
| import { importLeaves, importTimesheets, rearrangeTimesheets } from "@/app/api/timesheets/actions"; | |||||
| interface Props { | interface Props { | ||||
| } | } | ||||
| @@ -34,9 +34,12 @@ const ExcelFileImport: React.FC<Props> = async ({ }) => { | |||||
| case "importTimesheet": | case "importTimesheet": | ||||
| response = await importTimesheets(formData) | response = await importTimesheets(formData) | ||||
| break; | break; | ||||
| case "importLeave": | |||||
| response = await importLeaves(formData) | |||||
| break; | |||||
| } | } | ||||
| if (response === "Import Excel success") { | |||||
| if (response.includes("Import Excel success")) { | |||||
| successDialogWithContent(t("Import Success"), t(`${response}`), t) | successDialogWithContent(t("Import Success"), t(`${response}`), t) | ||||
| } else { | } else { | ||||
| errorDialogWithContent(t("Import Fail"), t(`${response}`), t) | errorDialogWithContent(t("Import Fail"), t(`${response}`), t) | ||||
| @@ -52,6 +55,37 @@ const ExcelFileImport: React.FC<Props> = async ({ }) => { | |||||
| return | return | ||||
| }, []) | }, []) | ||||
| const handleButtonClick = useCallback(async (event: React.MouseEvent<HTMLElement>) => { | |||||
| try { | |||||
| const targetId = event.currentTarget.id | |||||
| submitDialog(async () => { | |||||
| let response: String = "" | |||||
| switch (targetId) { | |||||
| case "rearrangeTimesheet": | |||||
| response = await rearrangeTimesheets() | |||||
| break; | |||||
| } | |||||
| if (response.includes("Rearrange success")) { | |||||
| successDialogWithContent(t("Rearrange Success"), t(`${response}`), t) | |||||
| } else { | |||||
| errorDialogWithContent(t("Rearrange Fail"), t(`${response}`), t) | |||||
| } | |||||
| }, t, | |||||
| { | |||||
| title: "Do you want to rearrange?", | |||||
| confirmButtonText: "Rearrange" | |||||
| }) | |||||
| } catch (err) { | |||||
| console.log(err) | |||||
| return false | |||||
| } | |||||
| }, []) | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Grid container rowGap={1.5}> | <Grid container rowGap={1.5}> | ||||
| @@ -93,6 +127,37 @@ const ExcelFileImport: React.FC<Props> = async ({ }) => { | |||||
| {t("Import Timesheet")} | {t("Import Timesheet")} | ||||
| </Button> | </Button> | ||||
| </Grid> | </Grid> | ||||
| <Grid container> | |||||
| <Button | |||||
| id="rearrangeTimesheet" | |||||
| variant="contained" | |||||
| color="error" | |||||
| startIcon={<FileUpload />} | |||||
| component="label" | |||||
| onClick={handleButtonClick} | |||||
| > | |||||
| {t("Rearrange Timesheet")} | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid container> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="info" | |||||
| startIcon={<FileUpload />} | |||||
| component="label" | |||||
| > | |||||
| <input | |||||
| id='importLeave' | |||||
| type='file' | |||||
| accept='.xlsx, .csv' | |||||
| hidden | |||||
| onChange={(event) => { | |||||
| handleExcelFileImportClick(event) | |||||
| }} | |||||
| /> | |||||
| {t("Import Leave")} | |||||
| </Button> | |||||
| </Grid> | |||||
| </Grid> | </Grid> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -27,11 +27,13 @@ import { | |||||
| import { manhourFormatter } from "@/app/utils/formatUtil"; | import { manhourFormatter } from "@/app/utils/formatUtil"; | ||||
| import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | ||||
| import uniqBy from "lodash/uniqBy"; | import uniqBy from "lodash/uniqBy"; | ||||
| import { TaskGroup } from "@/app/api/tasks"; | |||||
| import { Task, TaskGroup } from "@/app/api/tasks"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import isBetween from "dayjs/plugin/isBetween"; | import isBetween from "dayjs/plugin/isBetween"; | ||||
| import ProjectSelect from "../TimesheetTable/ProjectSelect"; | import ProjectSelect from "../TimesheetTable/ProjectSelect"; | ||||
| import TaskGroupSelect from "../TimesheetTable/TaskGroupSelect"; | |||||
| import TaskGroupSelect, { | |||||
| TaskGroupSelectWithoutProject, | |||||
| } from "../TimesheetTable/TaskGroupSelect"; | |||||
| import TaskSelect from "../TimesheetTable/TaskSelect"; | import TaskSelect from "../TimesheetTable/TaskSelect"; | ||||
| import { | import { | ||||
| DAILY_NORMAL_MAX_HOURS, | DAILY_NORMAL_MAX_HOURS, | ||||
| @@ -54,6 +56,7 @@ interface Props { | |||||
| assignedProjects: AssignedProject[]; | assignedProjects: AssignedProject[]; | ||||
| fastEntryEnabled?: boolean; | fastEntryEnabled?: boolean; | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| miscTasks: Task[]; | |||||
| } | } | ||||
| export type TimeLeaveRow = Partial< | export type TimeLeaveRow = Partial< | ||||
| @@ -71,6 +74,7 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
| isHoliday, | isHoliday, | ||||
| fastEntryEnabled, | fastEntryEnabled, | ||||
| leaveTypes, | leaveTypes, | ||||
| miscTasks, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| const taskGroupsByProject = useMemo(() => { | const taskGroupsByProject = useMemo(() => { | ||||
| @@ -92,6 +96,14 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
| }; | }; | ||||
| }, {}); | }, {}); | ||||
| }, [allProjects]); | }, [allProjects]); | ||||
| const taskGroupsWithoutProject = useMemo( | |||||
| () => | |||||
| uniqBy( | |||||
| miscTasks.map((t) => t.taskGroup), | |||||
| "id", | |||||
| ), | |||||
| [miscTasks], | |||||
| ); | |||||
| // To check for start / end planned dates | // To check for start / end planned dates | ||||
| const milestonesByProject = useMemo(() => { | const milestonesByProject = useMemo(() => { | ||||
| @@ -314,31 +326,62 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
| editable: true, | editable: true, | ||||
| renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow, number>) { | renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow, number>) { | ||||
| if (params.row.type === "timeEntry") { | if (params.row.type === "timeEntry") { | ||||
| return ( | |||||
| <TaskGroupSelect | |||||
| projectId={params.row.projectId} | |||||
| value={params.value} | |||||
| taskGroupsByProject={taskGroupsByProject} | |||||
| onTaskGroupSelect={(taskGroupId) => { | |||||
| params.api.setEditCellValue({ | |||||
| id: params.id, | |||||
| field: params.field, | |||||
| value: taskGroupId, | |||||
| }); | |||||
| params.api.setCellFocus(params.id, "taskId"); | |||||
| }} | |||||
| /> | |||||
| ); | |||||
| if (params.row.projectId) { | |||||
| return ( | |||||
| <TaskGroupSelect | |||||
| projectId={params.row.projectId} | |||||
| value={params.value} | |||||
| taskGroupsByProject={taskGroupsByProject} | |||||
| onTaskGroupSelect={(taskGroupId) => { | |||||
| params.api.setEditCellValue({ | |||||
| id: params.id, | |||||
| field: params.field, | |||||
| value: taskGroupId, | |||||
| }); | |||||
| params.api.setCellFocus(params.id, "taskId"); | |||||
| }} | |||||
| /> | |||||
| ); | |||||
| } else { | |||||
| return ( | |||||
| <TaskGroupSelectWithoutProject | |||||
| value={params.value} | |||||
| onTaskGroupSelect={(taskGroupId) => { | |||||
| params.api.setEditCellValue({ | |||||
| id: params.id, | |||||
| field: params.field, | |||||
| value: taskGroupId, | |||||
| }); | |||||
| params.api.setCellFocus(params.id, "taskId"); | |||||
| }} | |||||
| taskGroups={taskGroupsWithoutProject} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| } else { | } else { | ||||
| return <DisabledEdit />; | return <DisabledEdit />; | ||||
| } | } | ||||
| }, | }, | ||||
| valueFormatter(params) { | valueFormatter(params) { | ||||
| const taskGroups = params.id | |||||
| ? taskGroupsByProject[params.api.getRow(params.id).projectId] || [] | |||||
| : []; | |||||
| const taskGroup = taskGroups.find((tg) => tg.value === params.value); | |||||
| return taskGroup ? taskGroup.label : t("None"); | |||||
| if (!params.id) { | |||||
| return null; | |||||
| } | |||||
| const projectId = params.api.getRow(params.id).projectId; | |||||
| if (projectId) { | |||||
| const taskGroups = | |||||
| taskGroupsByProject[params.api.getRow(params.id).projectId] || []; | |||||
| const taskGroup = taskGroups.find( | |||||
| (tg) => tg.value === params.value, | |||||
| ); | |||||
| return taskGroup ? taskGroup.label : t("None"); | |||||
| } else { | |||||
| const taskGroupId = params.value; | |||||
| return ( | |||||
| taskGroupsWithoutProject.find((tg) => tg.id === taskGroupId) | |||||
| ?.name || t("None") | |||||
| ); | |||||
| } | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -362,6 +405,7 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
| }); | }); | ||||
| params.api.setCellFocus(params.id, "inputHours"); | params.api.setCellFocus(params.id, "inputHours"); | ||||
| }} | }} | ||||
| miscTasks={miscTasks} | |||||
| /> | /> | ||||
| ); | ); | ||||
| } else { | } else { | ||||
| @@ -373,12 +417,11 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
| ? params.api.getRow(params.id).projectId | ? params.api.getRow(params.id).projectId | ||||
| : undefined; | : undefined; | ||||
| const task = projectId | |||||
| ? allProjects | |||||
| .find((p) => p.id === projectId) | |||||
| ?.tasks.find((t) => t.id === params.value) | |||||
| : undefined; | |||||
| const task = ( | |||||
| projectId | |||||
| ? allProjects.find((p) => p.id === projectId)?.tasks || [] | |||||
| : miscTasks | |||||
| ).find((t) => t.id === params.value); | |||||
| return task ? task.name : t("None"); | return task ? task.name : t("None"); | ||||
| }, | }, | ||||
| }, | }, | ||||
| @@ -475,6 +518,8 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
| leaveTypes, | leaveTypes, | ||||
| assignedProjects, | assignedProjects, | ||||
| taskGroupsByProject, | taskGroupsByProject, | ||||
| taskGroupsWithoutProject, | |||||
| miscTasks, | |||||
| ], | ], | ||||
| ); | ); | ||||
| @@ -23,6 +23,7 @@ import { getHolidayForDate } from "@/app/utils/holidayUtils"; | |||||
| import FastTimeEntryModal from "../TimesheetTable/FastTimeEntryModal"; | import FastTimeEntryModal from "../TimesheetTable/FastTimeEntryModal"; | ||||
| import { LeaveType } from "@/app/api/timesheets"; | import { LeaveType } from "@/app/api/timesheets"; | ||||
| import LeaveEntryCard from "../LeaveTable/LeaveEntryCard"; | import LeaveEntryCard from "../LeaveTable/LeaveEntryCard"; | ||||
| import { Task } from "@/app/api/tasks"; | |||||
| interface Props { | interface Props { | ||||
| date: string; | date: string; | ||||
| @@ -31,6 +32,7 @@ interface Props { | |||||
| companyHolidays: HolidaysResult[]; | companyHolidays: HolidaysResult[]; | ||||
| fastEntryEnabled?: boolean; | fastEntryEnabled?: boolean; | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| miscTasks: Task[]; | |||||
| } | } | ||||
| const TimeLeaveMobileEntry: React.FC<Props> = ({ | const TimeLeaveMobileEntry: React.FC<Props> = ({ | ||||
| @@ -40,6 +42,7 @@ const TimeLeaveMobileEntry: React.FC<Props> = ({ | |||||
| companyHolidays, | companyHolidays, | ||||
| fastEntryEnabled, | fastEntryEnabled, | ||||
| leaveTypes, | leaveTypes, | ||||
| miscTasks, | |||||
| }) => { | }) => { | ||||
| const { | const { | ||||
| t, | t, | ||||
| @@ -222,7 +225,9 @@ const TimeLeaveMobileEntry: React.FC<Props> = ({ | |||||
| ? projectMap[entry.projectId] | ? projectMap[entry.projectId] | ||||
| : undefined; | : undefined; | ||||
| const task = project?.tasks.find((t) => t.id === entry.taskId); | |||||
| const task = (project ? project.tasks : miscTasks).find( | |||||
| (t) => t.id === entry.taskId, | |||||
| ); | |||||
| return ( | return ( | ||||
| <TimeEntryCard | <TimeEntryCard | ||||
| @@ -269,7 +274,7 @@ const TimeLeaveMobileEntry: React.FC<Props> = ({ | |||||
| onClose={closeEditTimeModal} | onClose={closeEditTimeModal} | ||||
| onSave={onSaveTimeEntry} | onSave={onSaveTimeEntry} | ||||
| isHoliday={Boolean(isHoliday)} | isHoliday={Boolean(isHoliday)} | ||||
| fastEntryEnabled={fastEntryEnabled} | |||||
| miscTasks={miscTasks} | |||||
| {...editTimeModalProps} | {...editTimeModalProps} | ||||
| /> | /> | ||||
| <LeaveEditModal | <LeaveEditModal | ||||
| @@ -38,6 +38,7 @@ import mapValues from "lodash/mapValues"; | |||||
| import DateHoursList from "../DateHoursTable/DateHoursList"; | import DateHoursList from "../DateHoursTable/DateHoursList"; | ||||
| import TimeLeaveInputTable from "./TimeLeaveInputTable"; | import TimeLeaveInputTable from "./TimeLeaveInputTable"; | ||||
| import TimeLeaveMobileEntry from "./TimeLeaveMobileEntry"; | import TimeLeaveMobileEntry from "./TimeLeaveMobileEntry"; | ||||
| import { Task } from "@/app/api/tasks"; | |||||
| interface Props { | interface Props { | ||||
| isOpen: boolean; | isOpen: boolean; | ||||
| @@ -50,6 +51,7 @@ interface Props { | |||||
| fastEntryEnabled?: boolean; | fastEntryEnabled?: boolean; | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| isFullTime: boolean; | isFullTime: boolean; | ||||
| miscTasks: Task[]; | |||||
| } | } | ||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| @@ -72,7 +74,8 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
| companyHolidays, | companyHolidays, | ||||
| fastEntryEnabled, | fastEntryEnabled, | ||||
| leaveTypes, | leaveTypes, | ||||
| isFullTime | |||||
| isFullTime, | |||||
| miscTasks, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| @@ -212,6 +215,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
| allProjects, | allProjects, | ||||
| fastEntryEnabled, | fastEntryEnabled, | ||||
| leaveTypes, | leaveTypes, | ||||
| miscTasks, | |||||
| }} | }} | ||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| @@ -261,6 +265,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
| companyHolidays, | companyHolidays, | ||||
| fastEntryEnabled, | fastEntryEnabled, | ||||
| leaveTypes, | leaveTypes, | ||||
| miscTasks, | |||||
| }} | }} | ||||
| errorComponent={errorComponent} | errorComponent={errorComponent} | ||||
| /> | /> | ||||
| @@ -68,7 +68,7 @@ const getID = () => { | |||||
| }; | }; | ||||
| const MISC_TASK_GROUP_ID = 5; | const MISC_TASK_GROUP_ID = 5; | ||||
| const FAST_ENTRY_TASK_ID = 40; | |||||
| const FAST_ENTRY_TASK_ID = 43; | |||||
| const FastTimeEntryModal: React.FC<Props> = ({ | const FastTimeEntryModal: React.FC<Props> = ({ | ||||
| onSave, | onSave, | ||||
| @@ -140,8 +140,8 @@ const FastTimeEntryModal: React.FC<Props> = ({ | |||||
| projectId, | projectId, | ||||
| inputHours: hour, | inputHours: hour, | ||||
| otHours: othour, | otHours: othour, | ||||
| taskGroupId: projectId ? MISC_TASK_GROUP_ID : undefined, | |||||
| taskId: projectId ? FAST_ENTRY_TASK_ID : undefined, | |||||
| taskGroupId: MISC_TASK_GROUP_ID, | |||||
| taskId: FAST_ENTRY_TASK_ID, | |||||
| remark, | remark, | ||||
| }; | }; | ||||
| }), | }), | ||||
| @@ -1,5 +1,5 @@ | |||||
| import React, { useCallback } from "react"; | import React, { useCallback } from "react"; | ||||
| import { MenuItem, Select, SelectChangeEvent } from "@mui/material"; | |||||
| import { MenuItem, MenuProps, Select, SelectChangeEvent } from "@mui/material"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { TaskGroup } from "@/app/api/tasks"; | import { TaskGroup } from "@/app/api/tasks"; | ||||
| @@ -16,6 +16,22 @@ interface Props { | |||||
| error?: boolean; | error?: boolean; | ||||
| } | } | ||||
| const menuProps: Partial<MenuProps> = { | |||||
| slotProps: { | |||||
| paper: { | |||||
| sx: { maxHeight: 400 }, | |||||
| }, | |||||
| }, | |||||
| anchorOrigin: { | |||||
| vertical: "bottom", | |||||
| horizontal: "left", | |||||
| }, | |||||
| transformOrigin: { | |||||
| vertical: "top", | |||||
| horizontal: "left", | |||||
| }, | |||||
| }; | |||||
| const TaskGroupSelect: React.FC<Props> = ({ | const TaskGroupSelect: React.FC<Props> = ({ | ||||
| value, | value, | ||||
| projectId, | projectId, | ||||
| @@ -43,21 +59,7 @@ const TaskGroupSelect: React.FC<Props> = ({ | |||||
| value={value || ""} | value={value || ""} | ||||
| onChange={onChange} | onChange={onChange} | ||||
| sx={{ width: "100%" }} | sx={{ width: "100%" }} | ||||
| MenuProps={{ | |||||
| slotProps: { | |||||
| paper: { | |||||
| sx: { maxHeight: 400 }, | |||||
| }, | |||||
| }, | |||||
| anchorOrigin: { | |||||
| vertical: "bottom", | |||||
| horizontal: "left", | |||||
| }, | |||||
| transformOrigin: { | |||||
| vertical: "top", | |||||
| horizontal: "left", | |||||
| }, | |||||
| }} | |||||
| MenuProps={menuProps} | |||||
| > | > | ||||
| {taskGroups.length === 0 && <MenuItem value={""}>{t("None")}</MenuItem>} | {taskGroups.length === 0 && <MenuItem value={""}>{t("None")}</MenuItem>} | ||||
| {taskGroups.map((taskGroup) => ( | {taskGroups.map((taskGroup) => ( | ||||
| @@ -73,4 +75,46 @@ const TaskGroupSelect: React.FC<Props> = ({ | |||||
| ); | ); | ||||
| }; | }; | ||||
| type TaskGroupSelectWithoutProjectProps = Pick< | |||||
| Props, | |||||
| "value" | "onTaskGroupSelect" | "error" | |||||
| > & { taskGroups: TaskGroup[] }; | |||||
| export const TaskGroupSelectWithoutProject: React.FC< | |||||
| TaskGroupSelectWithoutProjectProps | |||||
| > = ({ value, onTaskGroupSelect, error, taskGroups }) => { | |||||
| const { t } = useTranslation("home"); | |||||
| const onChange = useCallback( | |||||
| (event: SelectChangeEvent<number>) => { | |||||
| const newValue = event.target.value; | |||||
| onTaskGroupSelect(newValue); | |||||
| }, | |||||
| [onTaskGroupSelect], | |||||
| ); | |||||
| return ( | |||||
| <Select | |||||
| error={error} | |||||
| displayEmpty | |||||
| disabled={taskGroups.length === 0} | |||||
| value={value || ""} | |||||
| onChange={onChange} | |||||
| sx={{ width: "100%" }} | |||||
| MenuProps={menuProps} | |||||
| > | |||||
| {<MenuItem value={""}>{t("None")}</MenuItem>} | |||||
| {taskGroups.map((taskGroup) => ( | |||||
| <MenuItem | |||||
| key={taskGroup.id} | |||||
| value={taskGroup.id} | |||||
| sx={{ whiteSpace: "wrap" }} | |||||
| > | |||||
| {taskGroup.name} | |||||
| </MenuItem> | |||||
| ))} | |||||
| </Select> | |||||
| ); | |||||
| }; | |||||
| export default TaskGroupSelect; | export default TaskGroupSelect; | ||||
| @@ -2,6 +2,7 @@ import React, { useCallback } from "react"; | |||||
| import { MenuItem, Select, SelectChangeEvent } from "@mui/material"; | import { MenuItem, Select, SelectChangeEvent } from "@mui/material"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { ProjectWithTasks } from "@/app/api/projects"; | import { ProjectWithTasks } from "@/app/api/projects"; | ||||
| import { Task } from "@/app/api/tasks"; | |||||
| interface Props { | interface Props { | ||||
| allProjects: ProjectWithTasks[]; | allProjects: ProjectWithTasks[]; | ||||
| @@ -10,6 +11,7 @@ interface Props { | |||||
| taskGroupId: number | undefined; | taskGroupId: number | undefined; | ||||
| onTaskSelect: (taskId: number | string) => void; | onTaskSelect: (taskId: number | string) => void; | ||||
| error?: boolean; | error?: boolean; | ||||
| miscTasks?: Task[]; | |||||
| } | } | ||||
| const TaskSelect: React.FC<Props> = ({ | const TaskSelect: React.FC<Props> = ({ | ||||
| @@ -18,14 +20,15 @@ const TaskSelect: React.FC<Props> = ({ | |||||
| projectId, | projectId, | ||||
| taskGroupId, | taskGroupId, | ||||
| onTaskSelect, | onTaskSelect, | ||||
| error | |||||
| error, | |||||
| miscTasks, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| const project = allProjects.find((p) => p.id === projectId); | const project = allProjects.find((p) => p.id === projectId); | ||||
| const tasks = project | |||||
| ? project.tasks.filter((task) => task.taskGroup.id === taskGroupId) | |||||
| : []; | |||||
| const tasks = (project ? project.tasks : miscTasks || []).filter( | |||||
| (task) => task.taskGroup.id === taskGroupId, | |||||
| ); | |||||
| const onChange = useCallback( | const onChange = useCallback( | ||||
| (event: SelectChangeEvent<number>) => { | (event: SelectChangeEvent<number>) => { | ||||
| @@ -17,9 +17,11 @@ import { Controller, useForm } from "react-hook-form"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import ProjectSelect from "./ProjectSelect"; | import ProjectSelect from "./ProjectSelect"; | ||||
| import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | ||||
| import TaskGroupSelect from "./TaskGroupSelect"; | |||||
| import TaskGroupSelect, { | |||||
| TaskGroupSelectWithoutProject, | |||||
| } from "./TaskGroupSelect"; | |||||
| import TaskSelect from "./TaskSelect"; | import TaskSelect from "./TaskSelect"; | ||||
| import { TaskGroup } from "@/app/api/tasks"; | |||||
| import { Task, TaskGroup } from "@/app/api/tasks"; | |||||
| import uniqBy from "lodash/uniqBy"; | import uniqBy from "lodash/uniqBy"; | ||||
| import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | ||||
| import { shortDateFormatter } from "@/app/utils/formatUtil"; | import { shortDateFormatter } from "@/app/utils/formatUtil"; | ||||
| @@ -37,7 +39,7 @@ export interface Props extends Omit<ModalProps, "children"> { | |||||
| modalSx?: SxProps; | modalSx?: SxProps; | ||||
| recordDate?: string; | recordDate?: string; | ||||
| isHoliday?: boolean; | isHoliday?: boolean; | ||||
| fastEntryEnabled?: boolean; | |||||
| miscTasks: Task[]; | |||||
| } | } | ||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| @@ -63,7 +65,7 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| modalSx: mSx, | modalSx: mSx, | ||||
| recordDate, | recordDate, | ||||
| isHoliday, | isHoliday, | ||||
| fastEntryEnabled, | |||||
| miscTasks, | |||||
| }) => { | }) => { | ||||
| const { | const { | ||||
| t, | t, | ||||
| @@ -90,6 +92,15 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| }, {}); | }, {}); | ||||
| }, [allProjects]); | }, [allProjects]); | ||||
| const taskGroupsWithoutProject = useMemo( | |||||
| () => | |||||
| uniqBy( | |||||
| miscTasks.map((t) => t.taskGroup), | |||||
| "id", | |||||
| ), | |||||
| [miscTasks], | |||||
| ); | |||||
| const { | const { | ||||
| register, | register, | ||||
| control, | control, | ||||
| @@ -174,23 +185,35 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| <Controller | <Controller | ||||
| control={control} | control={control} | ||||
| name="taskGroupId" | name="taskGroupId" | ||||
| render={({ field }) => ( | |||||
| <TaskGroupSelect | |||||
| error={Boolean(formState.errors.taskGroupId)} | |||||
| projectId={projectId} | |||||
| taskGroupsByProject={taskGroupsByProject} | |||||
| value={field.value} | |||||
| onTaskGroupSelect={(newId) => { | |||||
| field.onChange(newId ?? null); | |||||
| }} | |||||
| /> | |||||
| )} | |||||
| render={({ field }) => | |||||
| projectId ? ( | |||||
| <TaskGroupSelect | |||||
| error={Boolean(formState.errors.taskGroupId)} | |||||
| projectId={projectId} | |||||
| taskGroupsByProject={taskGroupsByProject} | |||||
| value={field.value} | |||||
| onTaskGroupSelect={(newId) => { | |||||
| field.onChange(newId ?? null); | |||||
| }} | |||||
| /> | |||||
| ) : ( | |||||
| <TaskGroupSelectWithoutProject | |||||
| value={field.value} | |||||
| onTaskGroupSelect={(newId) => { | |||||
| field.onChange(newId ?? null); | |||||
| if (!newId) { | |||||
| setValue("taskId", undefined); | |||||
| } | |||||
| }} | |||||
| taskGroups={taskGroupsWithoutProject} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| rules={{ | rules={{ | ||||
| validate: (id) => { | validate: (id) => { | ||||
| if (!projectId) { | if (!projectId) { | ||||
| return !id; | |||||
| return true; | |||||
| } | } | ||||
| if (fastEntryEnabled) return true; | |||||
| const taskGroups = taskGroupsByProject[projectId]; | const taskGroups = taskGroupsByProject[projectId]; | ||||
| return taskGroups.some((tg) => tg.value === id); | return taskGroups.some((tg) => tg.value === id); | ||||
| }, | }, | ||||
| @@ -213,17 +236,23 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| onTaskSelect={(newId) => { | onTaskSelect={(newId) => { | ||||
| field.onChange(newId ?? null); | field.onChange(newId ?? null); | ||||
| }} | }} | ||||
| miscTasks={miscTasks} | |||||
| /> | /> | ||||
| )} | )} | ||||
| rules={{ | rules={{ | ||||
| validate: (id) => { | validate: (id) => { | ||||
| if (!projectId) { | |||||
| return !id; | |||||
| } | |||||
| if (fastEntryEnabled) return true; | |||||
| const projectTasks = allProjects.find((p) => p.id === projectId) | |||||
| ?.tasks; | |||||
| return Boolean(projectTasks?.some((task) => task.id === id)); | |||||
| const tasks = projectId | |||||
| ? allProjects.find((p) => p.id === projectId)?.tasks | |||||
| : miscTasks; | |||||
| return taskGroupId | |||||
| ? Boolean( | |||||
| tasks?.some( | |||||
| (task) => | |||||
| task.id === id && task.taskGroup.id === taskGroupId, | |||||
| ), | |||||
| ) | |||||
| : !id; | |||||
| }, | }, | ||||
| }} | }} | ||||
| /> | /> | ||||
| @@ -25,6 +25,7 @@ import { HolidaysResult } from "@/app/api/holidays"; | |||||
| import { TimesheetAmendmentModal } from "../TimesheetAmendment/TimesheetAmendmentModal"; | import { TimesheetAmendmentModal } from "../TimesheetAmendment/TimesheetAmendmentModal"; | ||||
| import TimeLeaveModal from "../TimeLeaveModal/TimeLeaveModal"; | import TimeLeaveModal from "../TimeLeaveModal/TimeLeaveModal"; | ||||
| import LeaveModal from "../LeaveModal"; | import LeaveModal from "../LeaveModal"; | ||||
| import { Task } from "@/app/api/tasks"; | |||||
| export interface Props { | export interface Props { | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| @@ -39,6 +40,7 @@ export interface Props { | |||||
| maintainNormalStaffWorkspaceAbility: boolean; | maintainNormalStaffWorkspaceAbility: boolean; | ||||
| maintainManagementStaffWorkspaceAbility: boolean; | maintainManagementStaffWorkspaceAbility: boolean; | ||||
| isFullTime: boolean; | isFullTime: boolean; | ||||
| miscTasks: Task[]; | |||||
| } | } | ||||
| const menuItemSx: SxProps = { | const menuItemSx: SxProps = { | ||||
| @@ -59,6 +61,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| maintainNormalStaffWorkspaceAbility, | maintainNormalStaffWorkspaceAbility, | ||||
| maintainManagementStaffWorkspaceAbility, | maintainManagementStaffWorkspaceAbility, | ||||
| isFullTime, | isFullTime, | ||||
| miscTasks | |||||
| }) => { | }) => { | ||||
| const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
| @@ -189,6 +192,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| timesheetRecords={defaultTimesheets} | timesheetRecords={defaultTimesheets} | ||||
| leaveRecords={defaultLeaveRecords} | leaveRecords={defaultLeaveRecords} | ||||
| isFullTime={isFullTime} | isFullTime={isFullTime} | ||||
| miscTasks={miscTasks} | |||||
| /> | /> | ||||
| <LeaveModal | <LeaveModal | ||||
| open={isLeaveCalendarVisible} | open={isLeaveCalendarVisible} | ||||
| @@ -17,6 +17,7 @@ import { | |||||
| MAINTAIN_NORMAL_STAFF_WORKSPACE, | MAINTAIN_NORMAL_STAFF_WORKSPACE, | ||||
| MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | ||||
| } from "@/middleware"; | } from "@/middleware"; | ||||
| import { fetchAllTasks } from "@/app/api/tasks"; | |||||
| const UserWorkspaceWrapper: React.FC = async () => { | const UserWorkspaceWrapper: React.FC = async () => { | ||||
| const [ | const [ | ||||
| @@ -30,6 +31,7 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
| holidays, | holidays, | ||||
| abilities, | abilities, | ||||
| userStaff, | userStaff, | ||||
| allTasks, | |||||
| ] = await Promise.all([ | ] = await Promise.all([ | ||||
| fetchTeamMemberLeaves(), | fetchTeamMemberLeaves(), | ||||
| fetchTeamMemberTimesheets(), | fetchTeamMemberTimesheets(), | ||||
| @@ -41,6 +43,7 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
| fetchHolidays(), | fetchHolidays(), | ||||
| getUserAbilities(), | getUserAbilities(), | ||||
| getUserStaff(), | getUserStaff(), | ||||
| fetchAllTasks(), | |||||
| ]); | ]); | ||||
| const fastEntryEnabled = abilities.includes( | const fastEntryEnabled = abilities.includes( | ||||
| @@ -54,6 +57,8 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
| ); | ); | ||||
| const isFullTime = userStaff?.employType === "FT"; | const isFullTime = userStaff?.employType === "FT"; | ||||
| const miscTasks = allTasks.filter((t) => t.taskGroup.id === 5); | |||||
| return ( | return ( | ||||
| <UserWorkspacePage | <UserWorkspacePage | ||||
| isFullTime={isFullTime} | isFullTime={isFullTime} | ||||
| @@ -65,6 +70,7 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
| defaultLeaveRecords={leaves} | defaultLeaveRecords={leaves} | ||||
| leaveTypes={leaveTypes} | leaveTypes={leaveTypes} | ||||
| holidays={holidays} | holidays={holidays} | ||||
| miscTasks={miscTasks} | |||||
| // Change to access check | // Change to access check | ||||
| fastEntryEnabled={fastEntryEnabled} | fastEntryEnabled={fastEntryEnabled} | ||||
| maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility} | maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility} | ||||