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