| @@ -11,7 +11,8 @@ export interface TimeEntry { | |||||
| projectId?: ProjectResult["id"]; | projectId?: ProjectResult["id"]; | ||||
| taskGroupId?: TaskGroup["id"]; | taskGroupId?: TaskGroup["id"]; | ||||
| taskId?: Task["id"]; | taskId?: Task["id"]; | ||||
| inputHours: number; | |||||
| inputHours?: number; | |||||
| otHours?: number; | |||||
| remark?: string; | remark?: string; | ||||
| } | } | ||||
| @@ -8,19 +8,24 @@ export const isValidTimeEntry = (entry: Partial<TimeEntry>): string => { | |||||
| // Test for errors | // Test for errors | ||||
| let error: keyof TimeEntry | "" = ""; | let error: keyof TimeEntry | "" = ""; | ||||
| // Either normal or other hours need to be inputted | |||||
| if (!entry.inputHours && !entry.otHours) { | |||||
| error = "inputHours"; | |||||
| } else if (entry.inputHours && entry.inputHours <= 0) { | |||||
| error = "inputHours"; | |||||
| } else if (entry.otHours && entry.otHours <= 0) { | |||||
| error = "otHours"; | |||||
| } | |||||
| // If there is a project id, there should also be taskGroupId, taskId, inputHours | // If there is a project id, there should also be taskGroupId, taskId, inputHours | ||||
| if (entry.projectId) { | if (entry.projectId) { | ||||
| if (!entry.taskGroupId) { | if (!entry.taskGroupId) { | ||||
| error = "taskGroupId"; | error = "taskGroupId"; | ||||
| } else if (!entry.taskId) { | } else if (!entry.taskId) { | ||||
| error = "taskId"; | error = "taskId"; | ||||
| } else if (!entry.inputHours || !(entry.inputHours >= 0)) { | |||||
| error = "inputHours"; | |||||
| } | } | ||||
| } else { | } else { | ||||
| if (!entry.inputHours || !(entry.inputHours >= 0)) { | |||||
| error = "inputHours"; | |||||
| } else if (!entry.remark) { | |||||
| if (!entry.remark) { | |||||
| error = "remark"; | error = "remark"; | ||||
| } | } | ||||
| } | } | ||||
| @@ -6,6 +6,7 @@ import { | |||||
| CardActions, | CardActions, | ||||
| CardContent, | CardContent, | ||||
| Modal, | Modal, | ||||
| ModalProps, | |||||
| SxProps, | SxProps, | ||||
| Typography, | Typography, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| @@ -86,8 +87,17 @@ const LeaveModal: React.FC<Props> = ({ | |||||
| onClose(); | onClose(); | ||||
| }, [defaultValues, formProps, onClose]); | }, [defaultValues, formProps, onClose]); | ||||
| const onModalClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
| (_, reason) => { | |||||
| if (reason !== "backdropClick") { | |||||
| onClose(); | |||||
| } | |||||
| }, | |||||
| [onClose], | |||||
| ); | |||||
| return ( | return ( | ||||
| <Modal open={isOpen} onClose={onClose}> | |||||
| <Modal open={isOpen} onClose={onModalClose}> | |||||
| <Card sx={modalSx}> | <Card sx={modalSx}> | ||||
| <FormProvider {...formProps}> | <FormProvider {...formProps}> | ||||
| <CardContent | <CardContent | ||||
| @@ -6,6 +6,7 @@ import { | |||||
| CardActions, | CardActions, | ||||
| CardContent, | CardContent, | ||||
| Modal, | Modal, | ||||
| ModalProps, | |||||
| SxProps, | SxProps, | ||||
| Typography, | Typography, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| @@ -91,8 +92,17 @@ const TimesheetModal: React.FC<Props> = ({ | |||||
| onClose(); | onClose(); | ||||
| }, [defaultValues, formProps, onClose]); | }, [defaultValues, formProps, onClose]); | ||||
| const onModalClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
| (_, reason) => { | |||||
| if (reason !== "backdropClick") { | |||||
| onClose(); | |||||
| } | |||||
| }, | |||||
| [onClose], | |||||
| ); | |||||
| return ( | return ( | ||||
| <Modal open={isOpen} onClose={onClose}> | |||||
| <Modal open={isOpen} onClose={onModalClose}> | |||||
| <Card sx={modalSx}> | <Card sx={modalSx}> | ||||
| <FormProvider {...formProps}> | <FormProvider {...formProps}> | ||||
| <CardContent | <CardContent | ||||
| @@ -211,7 +211,7 @@ const EntryInputTable: React.FC<Props> = ({ | |||||
| { | { | ||||
| field: "projectId", | field: "projectId", | ||||
| headerName: t("Project Code and Name"), | headerName: t("Project Code and Name"), | ||||
| width: 400, | |||||
| width: 300, | |||||
| editable: true, | editable: true, | ||||
| valueFormatter(params) { | valueFormatter(params) { | ||||
| const project = assignedProjects.find((p) => p.id === params.value); | const project = assignedProjects.find((p) => p.id === params.value); | ||||
| @@ -310,7 +310,17 @@ const EntryInputTable: React.FC<Props> = ({ | |||||
| editable: true, | editable: true, | ||||
| type: "number", | type: "number", | ||||
| valueFormatter(params) { | valueFormatter(params) { | ||||
| return manhourFormatter.format(params.value); | |||||
| return manhourFormatter.format(params.value || 0); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "otHours", | |||||
| headerName: t("Other Hours"), | |||||
| width: 150, | |||||
| editable: true, | |||||
| type: "number", | |||||
| valueFormatter(params) { | |||||
| return manhourFormatter.format(params.value || 0); | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -336,10 +346,9 @@ const EntryInputTable: React.FC<Props> = ({ | |||||
| useEffect(() => { | useEffect(() => { | ||||
| setValue(day, [ | setValue(day, [ | ||||
| ...entries | ...entries | ||||
| .filter((e) => !e._isNew && !e._error && e.id && e.inputHours) | |||||
| .filter((e) => !e._isNew && !e._error && e.id) | |||||
| .map(({ isPlanned, _error, _isNew, ...entry }) => ({ | .map(({ isPlanned, _error, _isNew, ...entry }) => ({ | ||||
| id: entry.id!, | id: entry.id!, | ||||
| inputHours: entry.inputHours!, | |||||
| ...entry, | ...entry, | ||||
| })), | })), | ||||
| ]); | ]); | ||||
| @@ -75,7 +75,10 @@ const DayRow: React.FC<{ | |||||
| const dayJsObj = dayjs(day); | const dayJsObj = dayjs(day); | ||||
| const [open, setOpen] = useState(false); | const [open, setOpen] = useState(false); | ||||
| const totalHours = entries.reduce((acc, entry) => acc + entry.inputHours, 0); | |||||
| const totalHours = entries.reduce( | |||||
| (acc, entry) => acc + (entry.inputHours || 0) + (entry.otHours || 0), | |||||
| 0, | |||||
| ); | |||||
| return ( | return ( | ||||
| <> | <> | ||||