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