|
|
@@ -21,6 +21,7 @@ import { manhourFormatter } from "@/app/utils/formatUtil"; |
|
|
|
import { AssignedProject } from "@/app/api/projects"; |
|
|
|
import uniqBy from "lodash/uniqBy"; |
|
|
|
import { TaskGroup } from "@/app/api/tasks"; |
|
|
|
import dayjs from "dayjs"; |
|
|
|
|
|
|
|
const mockProjects: AssignedProject[] = [ |
|
|
|
{ |
|
|
@@ -47,6 +48,16 @@ const mockProjects: AssignedProject[] = [ |
|
|
|
}, |
|
|
|
}, |
|
|
|
], |
|
|
|
milestones: { |
|
|
|
1: { |
|
|
|
startDate: "2000-01-01", |
|
|
|
endDate: "2100-01-01", |
|
|
|
}, |
|
|
|
2: { |
|
|
|
startDate: "2100-01-01", |
|
|
|
endDate: "2100-01-02", |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
id: 2, |
|
|
@@ -72,6 +83,16 @@ const mockProjects: AssignedProject[] = [ |
|
|
|
}, |
|
|
|
}, |
|
|
|
], |
|
|
|
milestones: { |
|
|
|
1: { |
|
|
|
startDate: "2000-01-01", |
|
|
|
endDate: "2100-01-01", |
|
|
|
}, |
|
|
|
3: { |
|
|
|
startDate: "2100-01-01", |
|
|
|
endDate: "2100-01-02", |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
id: 3, |
|
|
@@ -97,6 +118,16 @@ const mockProjects: AssignedProject[] = [ |
|
|
|
}, |
|
|
|
}, |
|
|
|
], |
|
|
|
milestones: { |
|
|
|
1: { |
|
|
|
startDate: "2000-01-01", |
|
|
|
endDate: "2100-01-01", |
|
|
|
}, |
|
|
|
4: { |
|
|
|
startDate: "2100-01-01", |
|
|
|
endDate: "2100-01-02", |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
]; |
|
|
|
|
|
|
@@ -104,6 +135,7 @@ type TimeEntryRow = Partial< |
|
|
|
TimeEntry & { |
|
|
|
_isNew: boolean; |
|
|
|
_error: string; |
|
|
|
isPlanned: boolean; |
|
|
|
id: string; |
|
|
|
taskGroupId: number; |
|
|
|
} |
|
|
@@ -131,6 +163,15 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { |
|
|
|
}, {}); |
|
|
|
}, []); |
|
|
|
|
|
|
|
// To check for start / end planned dates |
|
|
|
const milestonesByProject = useMemo(() => { |
|
|
|
return mockProjects.reduce<{ |
|
|
|
[projectId: AssignedProject["id"]]: AssignedProject["milestones"]; |
|
|
|
}>((acc, project) => { |
|
|
|
return { ...acc, [project.id]: { ...project.milestones } }; |
|
|
|
}, {}); |
|
|
|
}, []); |
|
|
|
|
|
|
|
const { getValues, setValue } = useFormContext<RecordTimesheetInput>(); |
|
|
|
const currentEntries = getValues(day); |
|
|
|
|
|
|
@@ -156,7 +197,9 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { |
|
|
|
id, |
|
|
|
"", |
|
|
|
) as TimeEntryRow; |
|
|
|
let error: keyof TimeEntry | "taskGroupId" | "" = ""; |
|
|
|
|
|
|
|
// Test for errrors |
|
|
|
let error: keyof TimeEntry | "" = ""; |
|
|
|
if (!row.projectId) { |
|
|
|
error = "projectId"; |
|
|
|
} else if (!row.taskGroupId) { |
|
|
@@ -167,10 +210,24 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { |
|
|
|
error = "inputHours"; |
|
|
|
} |
|
|
|
|
|
|
|
apiRef.current.updateRows([{ id, _error: error }]); |
|
|
|
// Test for warnings |
|
|
|
let isPlanned = false; |
|
|
|
if ( |
|
|
|
row.projectId && |
|
|
|
row.taskGroupId && |
|
|
|
milestonesByProject[row.projectId] |
|
|
|
) { |
|
|
|
const milestone = |
|
|
|
milestonesByProject[row.projectId][row.taskGroupId] || {}; |
|
|
|
const { startDate, endDate } = milestone; |
|
|
|
// Check if the current day is between the start and end date inclusively |
|
|
|
isPlanned = dayjs(day).isBetween(startDate, endDate, "day", "[]"); |
|
|
|
} |
|
|
|
|
|
|
|
apiRef.current.updateRows([{ id, _error: error, isPlanned }]); |
|
|
|
return !error; |
|
|
|
}, |
|
|
|
[apiRef], |
|
|
|
[apiRef, day, milestonesByProject], |
|
|
|
); |
|
|
|
|
|
|
|
const handleCancel = useCallback( |
|
|
@@ -363,6 +420,29 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { |
|
|
|
]); |
|
|
|
}, [getValues, entries, setValue, day]); |
|
|
|
|
|
|
|
const hasOutOfPlannedStages = entries.some( |
|
|
|
(entry) => entry.isPlanned !== undefined && !entry.isPlanned, |
|
|
|
); |
|
|
|
|
|
|
|
const footer = ( |
|
|
|
<Box display="flex" gap={2} alignItems="center"> |
|
|
|
<Button |
|
|
|
disableRipple |
|
|
|
variant="outlined" |
|
|
|
startIcon={<Add />} |
|
|
|
onClick={addRow} |
|
|
|
size="small" |
|
|
|
> |
|
|
|
{t("Record time")} |
|
|
|
</Button> |
|
|
|
{hasOutOfPlannedStages && ( |
|
|
|
<Typography color="warning.main" variant="body2"> |
|
|
|
{t("There are entries for stages out of planned dates!")} |
|
|
|
</Typography> |
|
|
|
)} |
|
|
|
</Box> |
|
|
|
); |
|
|
|
|
|
|
|
return ( |
|
|
|
<StyledDataGrid |
|
|
|
apiRef={apiRef} |
|
|
@@ -373,6 +453,10 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { |
|
|
|
border: "1px solid", |
|
|
|
borderColor: "error.main", |
|
|
|
}, |
|
|
|
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { |
|
|
|
border: "1px solid", |
|
|
|
borderColor: "warning.main", |
|
|
|
}, |
|
|
|
}} |
|
|
|
disableColumnMenu |
|
|
|
editMode="row" |
|
|
@@ -383,14 +467,24 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { |
|
|
|
processRowUpdate={processRowUpdate} |
|
|
|
columns={columns} |
|
|
|
getCellClassName={(params) => { |
|
|
|
return params.row._error === params.field ? "hasError" : ""; |
|
|
|
let classname = ""; |
|
|
|
if (params.row._error === params.field) { |
|
|
|
classname = "hasError"; |
|
|
|
} else if ( |
|
|
|
params.field === "taskGroupId" && |
|
|
|
params.row.isPlanned !== undefined && |
|
|
|
!params.row.isPlanned |
|
|
|
) { |
|
|
|
classname = "hasWarning"; |
|
|
|
} |
|
|
|
return classname; |
|
|
|
}} |
|
|
|
slots={{ |
|
|
|
footer: FooterToolbar, |
|
|
|
noRowsOverlay: NoRowsOverlay, |
|
|
|
}} |
|
|
|
slotProps={{ |
|
|
|
footer: { onAdd: addRow }, |
|
|
|
footer: { child: footer }, |
|
|
|
}} |
|
|
|
/> |
|
|
|
); |
|
|
@@ -410,21 +504,8 @@ const NoRowsOverlay: React.FC = () => { |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ onAdd }) => { |
|
|
|
const { t } = useTranslation(); |
|
|
|
return ( |
|
|
|
<GridToolbarContainer sx={{ p: 2 }}> |
|
|
|
<Button |
|
|
|
disableRipple |
|
|
|
variant="outlined" |
|
|
|
startIcon={<Add />} |
|
|
|
onClick={onAdd} |
|
|
|
size="small" |
|
|
|
> |
|
|
|
{t("Record time")} |
|
|
|
</Button> |
|
|
|
</GridToolbarContainer> |
|
|
|
); |
|
|
|
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { |
|
|
|
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; |
|
|
|
}; |
|
|
|
|
|
|
|
export default EntryInputTable; |