| @@ -57,8 +57,9 @@ const LeaveEditModal: React.FC<Props> = ({ | |||
| const valid = await trigger(); | |||
| if (valid) { | |||
| onSave(getValues()); | |||
| reset(); | |||
| } | |||
| }, [getValues, onSave, trigger]); | |||
| }, [getValues, onSave, reset, trigger]); | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (...args) => { | |||
| @@ -86,93 +86,100 @@ const MobileLeaveEntry: React.FC<Props> = ({ date, leaveTypes }) => { | |||
| ); | |||
| return ( | |||
| <Box | |||
| marginInline={2} | |||
| flex={1} | |||
| display="flex" | |||
| flexDirection="column" | |||
| gap={2} | |||
| > | |||
| <> | |||
| <Typography | |||
| paddingInline={2} | |||
| variant="overline" | |||
| color={dayJsObj.day() === 0 ? "error.main" : undefined} | |||
| > | |||
| {shortDateFormatter(language).format(dayJsObj.toDate())} | |||
| </Typography> | |||
| {currentEntries.length ? ( | |||
| currentEntries.map((entry, index) => { | |||
| return ( | |||
| <Card key={`${entry.id}-${index}`} sx={{ marginInline: 1 }}> | |||
| <CardContent | |||
| sx={{ | |||
| padding: 2, | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| gap: 2, | |||
| "&:last-child": { | |||
| paddingBottom: 2, | |||
| }, | |||
| }} | |||
| <Box | |||
| paddingInline={2} | |||
| flex={1} | |||
| display="flex" | |||
| flexDirection="column" | |||
| gap={2} | |||
| overflow="scroll" | |||
| > | |||
| {currentEntries.length ? ( | |||
| currentEntries.map((entry, index) => { | |||
| return ( | |||
| <Card | |||
| key={`${entry.id}-${index}`} | |||
| sx={{ marginInline: 1, overflow: "visible" }} | |||
| > | |||
| <Box | |||
| display="flex" | |||
| justifyContent="space-between" | |||
| alignItems="flex-start" | |||
| <CardContent | |||
| sx={{ | |||
| padding: 2, | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| gap: 2, | |||
| "&:last-child": { | |||
| paddingBottom: 2, | |||
| }, | |||
| }} | |||
| > | |||
| <Box> | |||
| <Typography | |||
| variant="body2" | |||
| component="div" | |||
| fontWeight="bold" | |||
| > | |||
| {leaveTypeMap[entry.leaveTypeId].name} | |||
| </Typography> | |||
| <Typography component="p"> | |||
| {manhourFormatter.format(entry.inputHours)} | |||
| </Typography> | |||
| </Box> | |||
| <IconButton | |||
| size="small" | |||
| color="primary" | |||
| onClick={openEditModal(entry)} | |||
| <Box | |||
| display="flex" | |||
| justifyContent="space-between" | |||
| alignItems="flex-start" | |||
| > | |||
| <Edit /> | |||
| </IconButton> | |||
| </Box> | |||
| {entry.remark && ( | |||
| <Box> | |||
| <Typography | |||
| variant="body2" | |||
| component="div" | |||
| fontWeight="bold" | |||
| <Box> | |||
| <Typography | |||
| variant="body2" | |||
| component="div" | |||
| fontWeight="bold" | |||
| > | |||
| {leaveTypeMap[entry.leaveTypeId].name} | |||
| </Typography> | |||
| <Typography component="p"> | |||
| {manhourFormatter.format(entry.inputHours)} | |||
| </Typography> | |||
| </Box> | |||
| <IconButton | |||
| size="small" | |||
| color="primary" | |||
| onClick={openEditModal(entry)} | |||
| > | |||
| {t("Remark")} | |||
| </Typography> | |||
| <Typography component="p">{entry.remark}</Typography> | |||
| <Edit /> | |||
| </IconButton> | |||
| </Box> | |||
| )} | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }) | |||
| ) : ( | |||
| <Typography variant="body2" display="block"> | |||
| {t("Add some leave entries!")} | |||
| </Typography> | |||
| )} | |||
| <Box> | |||
| <Button startIcon={<Add />} onClick={openEditModal()}> | |||
| {t("Record leave")} | |||
| </Button> | |||
| {entry.remark && ( | |||
| <Box> | |||
| <Typography | |||
| variant="body2" | |||
| component="div" | |||
| fontWeight="bold" | |||
| > | |||
| {t("Remark")} | |||
| </Typography> | |||
| <Typography component="p">{entry.remark}</Typography> | |||
| </Box> | |||
| )} | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }) | |||
| ) : ( | |||
| <Typography variant="body2" display="block"> | |||
| {t("Add some leave entries!")} | |||
| </Typography> | |||
| )} | |||
| <Box> | |||
| <Button startIcon={<Add />} onClick={openEditModal()}> | |||
| {t("Record leave")} | |||
| </Button> | |||
| </Box> | |||
| <LeaveEditModal | |||
| leaveTypes={leaveTypes} | |||
| open={editModalOpen} | |||
| onClose={closeEditModal} | |||
| onSave={onSaveEntry} | |||
| {...editModalProps} | |||
| /> | |||
| </Box> | |||
| <LeaveEditModal | |||
| leaveTypes={leaveTypes} | |||
| open={editModalOpen} | |||
| onClose={closeEditModal} | |||
| onSave={onSaveEntry} | |||
| {...editModalProps} | |||
| /> | |||
| </Box> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -277,7 +277,6 @@ const EntryInputTable: React.FC<Props> = ({ | |||
| projectId={params.row.projectId} | |||
| taskGroupId={params.row.taskGroupId} | |||
| allProjects={allProjects} | |||
| editCellProps={params} | |||
| onTaskSelect={(taskId) => { | |||
| params.api.setEditCellValue({ | |||
| id: params.id, | |||
| @@ -10,10 +10,13 @@ import { | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import dayjs from "dayjs"; | |||
| import React from "react"; | |||
| import React, { useCallback, useMemo, useState } from "react"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | |||
| import TimesheetEditModal, { | |||
| Props as TimesheetEditModalProps, | |||
| } from "./TimesheetEditModal"; | |||
| interface Props { | |||
| date: string; | |||
| @@ -30,11 +33,200 @@ const MobileTimesheetEntry: React.FC<Props> = ({ | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation("home"); | |||
| const projectMap = useMemo(() => { | |||
| return allProjects.reduce<{ | |||
| [id: ProjectWithTasks["id"]]: ProjectWithTasks; | |||
| }>((acc, project) => { | |||
| return { ...acc, [project.id]: project }; | |||
| }, {}); | |||
| }, [allProjects]); | |||
| const dayJsObj = dayjs(date); | |||
| const { watch, setValue } = useFormContext<RecordTimesheetInput>(); | |||
| const currentEntries = watch(date); | |||
| return null; | |||
| // Edit modal | |||
| const [editModalProps, setEditModalProps] = useState< | |||
| Partial<TimesheetEditModalProps> | |||
| >({}); | |||
| const [editModalOpen, setEditModalOpen] = useState(false); | |||
| const openEditModal = useCallback( | |||
| (defaultValues?: TimeEntry) => () => { | |||
| setEditModalProps({ | |||
| defaultValues, | |||
| onDelete: defaultValues | |||
| ? () => { | |||
| setValue( | |||
| date, | |||
| currentEntries.filter((entry) => entry.id !== defaultValues.id), | |||
| ); | |||
| setEditModalOpen(false); | |||
| } | |||
| : undefined, | |||
| }); | |||
| setEditModalOpen(true); | |||
| }, | |||
| [currentEntries, date, setValue], | |||
| ); | |||
| const closeEditModal = useCallback(() => { | |||
| setEditModalOpen(false); | |||
| }, []); | |||
| const onSaveEntry = useCallback( | |||
| (entry: TimeEntry) => { | |||
| const existingEntry = currentEntries.find((e) => e.id === entry.id); | |||
| if (existingEntry) { | |||
| setValue( | |||
| date, | |||
| currentEntries.map((e) => ({ | |||
| ...(e.id === existingEntry.id ? entry : e), | |||
| })), | |||
| ); | |||
| } else { | |||
| setValue(date, [...currentEntries, entry]); | |||
| } | |||
| setEditModalOpen(false); | |||
| }, | |||
| [currentEntries, date, setValue], | |||
| ); | |||
| return ( | |||
| <> | |||
| <Typography | |||
| paddingInline={2} | |||
| variant="overline" | |||
| color={dayJsObj.day() === 0 ? "error.main" : undefined} | |||
| > | |||
| {shortDateFormatter(language).format(dayJsObj.toDate())} | |||
| </Typography> | |||
| <Box | |||
| paddingInline={2} | |||
| flex={1} | |||
| display="flex" | |||
| flexDirection="column" | |||
| gap={2} | |||
| overflow="scroll" | |||
| > | |||
| {currentEntries.length ? ( | |||
| currentEntries.map((entry, index) => { | |||
| const project = entry.projectId | |||
| ? projectMap[entry.projectId] | |||
| : undefined; | |||
| const task = project?.tasks.find((t) => t.id === entry.taskId); | |||
| return ( | |||
| <Card | |||
| key={`${entry.id}-${index}`} | |||
| sx={{ marginInline: 1, overflow: "visible" }} | |||
| > | |||
| <CardContent | |||
| sx={{ | |||
| padding: 2, | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| gap: 2, | |||
| "&:last-child": { | |||
| paddingBottom: 2, | |||
| }, | |||
| }} | |||
| > | |||
| <Box | |||
| display="flex" | |||
| justifyContent="space-between" | |||
| alignItems="flex-start" | |||
| gap={2} | |||
| > | |||
| <Box> | |||
| <Typography | |||
| variant="body2" | |||
| component="div" | |||
| fontWeight="bold" | |||
| > | |||
| {project | |||
| ? `${project.code} - ${project.name}` | |||
| : t("Non-billable Task")} | |||
| </Typography> | |||
| {task && ( | |||
| <Typography variant="body2" component="div"> | |||
| {task.name} | |||
| </Typography> | |||
| )} | |||
| </Box> | |||
| <IconButton | |||
| size="small" | |||
| color="primary" | |||
| onClick={openEditModal(entry)} | |||
| > | |||
| <Edit /> | |||
| </IconButton> | |||
| </Box> | |||
| <Box display="flex" gap={2}> | |||
| <Box> | |||
| <Typography | |||
| variant="body2" | |||
| component="div" | |||
| fontWeight="bold" | |||
| > | |||
| {t("Hours")} | |||
| </Typography> | |||
| <Typography component="p"> | |||
| {manhourFormatter.format(entry.inputHours || 0)} | |||
| </Typography> | |||
| </Box> | |||
| <Box> | |||
| <Typography | |||
| variant="body2" | |||
| component="div" | |||
| fontWeight="bold" | |||
| > | |||
| {t("Other Hours")} | |||
| </Typography> | |||
| <Typography component="p"> | |||
| {manhourFormatter.format(entry.otHours || 0)} | |||
| </Typography> | |||
| </Box> | |||
| </Box> | |||
| {entry.remark && ( | |||
| <Box> | |||
| <Typography | |||
| variant="body2" | |||
| component="div" | |||
| fontWeight="bold" | |||
| > | |||
| {t("Remark")} | |||
| </Typography> | |||
| <Typography component="p">{entry.remark}</Typography> | |||
| </Box> | |||
| )} | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }) | |||
| ) : ( | |||
| <Typography variant="body2" display="block"> | |||
| {t("Add some time entries!")} | |||
| </Typography> | |||
| )} | |||
| <Box> | |||
| <Button startIcon={<Add />} onClick={openEditModal()}> | |||
| {t("Record leave")} | |||
| </Button> | |||
| </Box> | |||
| <TimesheetEditModal | |||
| allProjects={allProjects} | |||
| assignedProjects={assignedProjects} | |||
| open={editModalOpen} | |||
| onClose={closeEditModal} | |||
| onSave={onSaveEntry} | |||
| {...editModalProps} | |||
| /> | |||
| </Box> | |||
| </> | |||
| ); | |||
| }; | |||
| export default MobileTimesheetEntry; | |||
| @@ -1,9 +1,11 @@ | |||
| import React, { useCallback, useMemo } from "react"; | |||
| import { | |||
| Autocomplete, | |||
| ListSubheader, | |||
| MenuItem, | |||
| Select, | |||
| SelectChangeEvent, | |||
| TextField, | |||
| } from "@mui/material"; | |||
| import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | |||
| import { useTranslation } from "react-i18next"; | |||
| @@ -16,6 +18,49 @@ interface Props { | |||
| onProjectSelect: (projectId: number | string) => void; | |||
| } | |||
| // const AutocompleteProjectSelect: React.FC<Props> = ({ | |||
| // allProjects, | |||
| // assignedProjects, | |||
| // value, | |||
| // onProjectSelect, | |||
| // }) => { | |||
| // const { t } = useTranslation("home"); | |||
| // const nonAssignedProjects = useMemo(() => { | |||
| // return differenceBy(allProjects, assignedProjects, "id"); | |||
| // }, [allProjects, assignedProjects]); | |||
| // const options = useMemo(() => { | |||
| // return [ | |||
| // { | |||
| // value: "", | |||
| // label: t("None"), | |||
| // group: "non-billable", | |||
| // }, | |||
| // ...assignedProjects.map((p) => ({ | |||
| // value: p.id, | |||
| // label: `${p.code} - ${p.name}`, | |||
| // group: "assigned", | |||
| // })), | |||
| // ...nonAssignedProjects.map((p) => ({ | |||
| // value: p.id, | |||
| // label: `${p.code} - ${p.name}`, | |||
| // group: "non-assigned", | |||
| // })), | |||
| // ]; | |||
| // }, [assignedProjects, nonAssignedProjects, t]); | |||
| // return ( | |||
| // <Autocomplete | |||
| // disableClearable | |||
| // fullWidth | |||
| // groupBy={(option) => option.group} | |||
| // getOptionLabel={(option) => option.label} | |||
| // options={options} | |||
| // renderInput={(params) => <TextField {...params} />} | |||
| // /> | |||
| // ); | |||
| // }; | |||
| const ProjectSelect: React.FC<Props> = ({ | |||
| allProjects, | |||
| assignedProjects, | |||
| @@ -68,6 +113,7 @@ const ProjectSelect: React.FC<Props> = ({ | |||
| <MenuItem | |||
| key={project.id} | |||
| value={project.id} | |||
| sx={{ whiteSpace: "wrap" }} | |||
| >{`${project.code} - ${project.name}`}</MenuItem> | |||
| )), | |||
| ]} | |||
| @@ -79,6 +125,7 @@ const ProjectSelect: React.FC<Props> = ({ | |||
| <MenuItem | |||
| key={project.id} | |||
| value={project.id} | |||
| sx={{ whiteSpace: "wrap" }} | |||
| >{`${project.code} - ${project.name}`}</MenuItem> | |||
| )), | |||
| ]} | |||
| @@ -13,6 +13,7 @@ interface Props { | |||
| projectId: number | undefined; | |||
| value: number | undefined; | |||
| onTaskGroupSelect: (taskGroupId: number | string) => void; | |||
| error?: boolean; | |||
| } | |||
| const TaskGroupSelect: React.FC<Props> = ({ | |||
| @@ -20,6 +21,7 @@ const TaskGroupSelect: React.FC<Props> = ({ | |||
| projectId, | |||
| onTaskGroupSelect, | |||
| taskGroupsByProject, | |||
| error, | |||
| }) => { | |||
| const { t } = useTranslation("home"); | |||
| @@ -35,6 +37,7 @@ const TaskGroupSelect: React.FC<Props> = ({ | |||
| return ( | |||
| <Select | |||
| error={error} | |||
| displayEmpty | |||
| disabled={taskGroups.length === 0} | |||
| value={value || ""} | |||
| @@ -58,7 +61,11 @@ const TaskGroupSelect: React.FC<Props> = ({ | |||
| > | |||
| {taskGroups.length === 0 && <MenuItem value={""}>{t("None")}</MenuItem>} | |||
| {taskGroups.map((taskGroup) => ( | |||
| <MenuItem key={taskGroup.value} value={taskGroup.value}> | |||
| <MenuItem | |||
| key={taskGroup.value} | |||
| value={taskGroup.value} | |||
| sx={{ whiteSpace: "wrap" }} | |||
| > | |||
| {taskGroup.label} | |||
| </MenuItem> | |||
| ))} | |||
| @@ -1,7 +1,5 @@ | |||
| import React, { useCallback } from "react"; | |||
| import { MenuItem, Select, SelectChangeEvent } from "@mui/material"; | |||
| import { GridRenderEditCellParams } from "@mui/x-data-grid"; | |||
| import { TimeEntryRow } from "./EntryInputTable"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { ProjectWithTasks } from "@/app/api/projects"; | |||
| @@ -10,8 +8,8 @@ interface Props { | |||
| value: number | undefined; | |||
| projectId: number | undefined; | |||
| taskGroupId: number | undefined; | |||
| editCellProps: GridRenderEditCellParams<TimeEntryRow, number>; | |||
| onTaskSelect: (taskId: number | string) => void; | |||
| error?: boolean; | |||
| } | |||
| const TaskSelect: React.FC<Props> = ({ | |||
| @@ -20,6 +18,7 @@ const TaskSelect: React.FC<Props> = ({ | |||
| projectId, | |||
| taskGroupId, | |||
| onTaskSelect, | |||
| error | |||
| }) => { | |||
| const { t } = useTranslation("home"); | |||
| @@ -38,6 +37,7 @@ const TaskSelect: React.FC<Props> = ({ | |||
| return ( | |||
| <Select | |||
| error={error} | |||
| displayEmpty | |||
| disabled={tasks.length === 0} | |||
| value={value || ""} | |||
| @@ -61,7 +61,7 @@ const TaskSelect: React.FC<Props> = ({ | |||
| > | |||
| {tasks.length === 0 && <MenuItem value={""}>{t("None")}</MenuItem>} | |||
| {tasks.map((task) => ( | |||
| <MenuItem key={task.id} value={task.id}> | |||
| <MenuItem key={task.id} value={task.id} sx={{ whiteSpace: "wrap" }}> | |||
| {task.name} | |||
| </MenuItem> | |||
| ))} | |||
| @@ -0,0 +1,247 @@ | |||
| import { TimeEntry } from "@/app/api/timesheets/actions"; | |||
| import { Check, Delete } from "@mui/icons-material"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| FormControl, | |||
| InputLabel, | |||
| Modal, | |||
| ModalProps, | |||
| Paper, | |||
| SxProps, | |||
| TextField, | |||
| } from "@mui/material"; | |||
| import React, { useCallback, useEffect, useMemo } from "react"; | |||
| import { Controller, useForm } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import ProjectSelect from "./ProjectSelect"; | |||
| import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | |||
| import TaskGroupSelect from "./TaskGroupSelect"; | |||
| import TaskSelect from "./TaskSelect"; | |||
| import { TaskGroup } from "@/app/api/tasks"; | |||
| import uniqBy from "lodash/uniqBy"; | |||
| export interface Props extends Omit<ModalProps, "children"> { | |||
| onSave: (leaveEntry: TimeEntry) => void; | |||
| onDelete?: () => void; | |||
| defaultValues?: Partial<TimeEntry>; | |||
| allProjects: ProjectWithTasks[]; | |||
| assignedProjects: AssignedProject[]; | |||
| } | |||
| const modalSx: SxProps = { | |||
| position: "absolute", | |||
| top: "50%", | |||
| left: "50%", | |||
| transform: "translate(-50%, -50%)", | |||
| width: "90%", | |||
| maxHeight: "90%", | |||
| padding: 3, | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| gap: 2, | |||
| }; | |||
| const TimesheetEditModal: React.FC<Props> = ({ | |||
| onSave, | |||
| onDelete, | |||
| open, | |||
| onClose, | |||
| defaultValues, | |||
| allProjects, | |||
| assignedProjects, | |||
| }) => { | |||
| const { t } = useTranslation("home"); | |||
| const taskGroupsByProject = useMemo(() => { | |||
| return allProjects.reduce<{ | |||
| [projectId: AssignedProject["id"]]: { | |||
| value: TaskGroup["id"]; | |||
| label: string; | |||
| }[]; | |||
| }>((acc, project) => { | |||
| return { | |||
| ...acc, | |||
| [project.id]: uniqBy( | |||
| project.tasks.map((t) => ({ | |||
| value: t.taskGroup.id, | |||
| label: t.taskGroup.name, | |||
| })), | |||
| "value", | |||
| ), | |||
| }; | |||
| }, {}); | |||
| }, [allProjects]); | |||
| const { | |||
| register, | |||
| control, | |||
| reset, | |||
| getValues, | |||
| setValue, | |||
| trigger, | |||
| formState, | |||
| watch, | |||
| } = useForm<TimeEntry>(); | |||
| useEffect(() => { | |||
| reset(defaultValues ?? { id: Date.now() }); | |||
| }, [defaultValues, reset]); | |||
| const saveHandler = useCallback(async () => { | |||
| const valid = await trigger(); | |||
| if (valid) { | |||
| onSave(getValues()); | |||
| reset(); | |||
| } | |||
| }, [getValues, onSave, reset, trigger]); | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (...args) => { | |||
| onClose?.(...args); | |||
| reset(); | |||
| }, | |||
| [onClose, reset], | |||
| ); | |||
| const projectId = watch("projectId"); | |||
| const taskGroupId = watch("taskGroupId"); | |||
| const otHours = watch("otHours"); | |||
| return ( | |||
| <Modal open={open} onClose={closeHandler}> | |||
| <Paper sx={modalSx}> | |||
| <FormControl fullWidth> | |||
| <InputLabel shrink>{t("Project Code and Name")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="projectId" | |||
| render={({ field }) => ( | |||
| <ProjectSelect | |||
| allProjects={allProjects} | |||
| assignedProjects={assignedProjects} | |||
| value={field.value} | |||
| onProjectSelect={(newId) => { | |||
| field.onChange(newId ?? null); | |||
| const firstTaskGroup = ( | |||
| typeof newId === "number" ? taskGroupsByProject[newId] : [] | |||
| )[0]; | |||
| setValue("taskGroupId", firstTaskGroup?.value); | |||
| setValue("taskId", undefined); | |||
| }} | |||
| /> | |||
| )} | |||
| rules={{ deps: ["taskGroupId", "taskId"] }} | |||
| /> | |||
| </FormControl> | |||
| <FormControl fullWidth> | |||
| <InputLabel shrink>{t("Stage")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="taskGroupId" | |||
| render={({ field }) => ( | |||
| <TaskGroupSelect | |||
| error={Boolean(formState.errors.taskGroupId)} | |||
| projectId={projectId} | |||
| taskGroupsByProject={taskGroupsByProject} | |||
| value={field.value} | |||
| onTaskGroupSelect={(newId) => { | |||
| field.onChange(newId ?? null); | |||
| }} | |||
| /> | |||
| )} | |||
| rules={{ | |||
| validate: (id) => { | |||
| if (!projectId) { | |||
| return !id; | |||
| } | |||
| const taskGroups = taskGroupsByProject[projectId]; | |||
| return taskGroups.some((tg) => tg.value === id); | |||
| }, | |||
| deps: ["taskId"], | |||
| }} | |||
| /> | |||
| </FormControl> | |||
| <FormControl fullWidth> | |||
| <InputLabel shrink>{t("Task")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="taskId" | |||
| render={({ field }) => ( | |||
| <TaskSelect | |||
| error={Boolean(formState.errors.taskId)} | |||
| projectId={projectId} | |||
| taskGroupId={taskGroupId} | |||
| allProjects={allProjects} | |||
| value={field.value} | |||
| onTaskSelect={(newId) => { | |||
| field.onChange(newId ?? null); | |||
| }} | |||
| /> | |||
| )} | |||
| rules={{ | |||
| validate: (id) => { | |||
| if (!projectId) { | |||
| return !id; | |||
| } | |||
| const projectTasks = allProjects.find((p) => p.id === projectId) | |||
| ?.tasks; | |||
| return Boolean(projectTasks?.some((task) => task.id === id)); | |||
| }, | |||
| }} | |||
| /> | |||
| </FormControl> | |||
| <TextField | |||
| type="number" | |||
| label={t("Hours")} | |||
| fullWidth | |||
| {...register("inputHours", { | |||
| valueAsNumber: true, | |||
| validate: (value) => Boolean(value || otHours), | |||
| })} | |||
| error={Boolean(formState.errors.inputHours)} | |||
| /> | |||
| <TextField | |||
| type="number" | |||
| label={t("Other Hours")} | |||
| fullWidth | |||
| {...register("otHours", { | |||
| valueAsNumber: true, | |||
| })} | |||
| error={Boolean(formState.errors.otHours)} | |||
| /> | |||
| <TextField | |||
| label={t("Remark")} | |||
| fullWidth | |||
| multiline | |||
| rows={2} | |||
| error={Boolean(formState.errors.remark)} | |||
| {...register("remark", { | |||
| validate: (value) => Boolean(projectId || value), | |||
| })} | |||
| /> | |||
| <Box display="flex" justifyContent="flex-end" gap={1}> | |||
| {onDelete && ( | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<Delete />} | |||
| color="error" | |||
| onClick={onDelete} | |||
| > | |||
| {t("Delete")} | |||
| </Button> | |||
| )} | |||
| <Button | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| onClick={saveHandler} | |||
| > | |||
| {t("Save")} | |||
| </Button> | |||
| </Box> | |||
| </Paper> | |||
| </Modal> | |||
| ); | |||
| }; | |||
| export default TimesheetEditModal; | |||