|
|
@@ -6,7 +6,6 @@ import { |
|
|
|
GridCellParams, |
|
|
|
GridColDef, |
|
|
|
GridEditInputCell, |
|
|
|
GridEventListener, |
|
|
|
GridRenderEditCellParams, |
|
|
|
GridRowId, |
|
|
|
GridRowModel, |
|
|
@@ -67,6 +66,22 @@ export type TimeLeaveRow = Partial< |
|
|
|
} |
|
|
|
>; |
|
|
|
|
|
|
|
class ProcessRowUpdateError extends Error { |
|
|
|
public readonly rowId: GridRowId; |
|
|
|
public readonly errors: TimeEntryError | LeaveEntryError | undefined; |
|
|
|
constructor( |
|
|
|
rowId: GridRowId, |
|
|
|
message?: string, |
|
|
|
errors?: TimeEntryError | LeaveEntryError, |
|
|
|
) { |
|
|
|
super(message); |
|
|
|
this.rowId = rowId; |
|
|
|
this.errors = errors; |
|
|
|
|
|
|
|
Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const TimeLeaveInputTable: React.FC<Props> = ({ |
|
|
|
day, |
|
|
|
allProjects, |
|
|
@@ -133,38 +148,32 @@ const TimeLeaveInputTable: React.FC<Props> = ({ |
|
|
|
}, []); |
|
|
|
|
|
|
|
const validateRow = useCallback( |
|
|
|
(id: GridRowId) => { |
|
|
|
const row = apiRef.current.getRowWithUpdatedValues( |
|
|
|
id, |
|
|
|
"", |
|
|
|
) as TimeLeaveRow; |
|
|
|
|
|
|
|
// Test for warnings |
|
|
|
(row: TimeLeaveRow) => { |
|
|
|
if (row.type === "timeEntry") { |
|
|
|
const error = validateTimeEntry(row, isHoliday); |
|
|
|
let _isPlanned; |
|
|
|
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; |
|
|
|
} else if (row.type === "leaveEntry") { |
|
|
|
const error = validateLeaveEntry(row, isHoliday); |
|
|
|
apiRef.current.updateRows([{ id, _error: error }]); |
|
|
|
return !error; |
|
|
|
return validateTimeEntry(row, isHoliday); |
|
|
|
} else { |
|
|
|
return false; |
|
|
|
return validateLeaveEntry(row, isHoliday); |
|
|
|
} |
|
|
|
}, |
|
|
|
[apiRef, day, isHoliday, milestonesByProject], |
|
|
|
[isHoliday], |
|
|
|
); |
|
|
|
|
|
|
|
const verifyIsPlanned = useCallback( |
|
|
|
(row: TimeLeaveRow) => { |
|
|
|
if ( |
|
|
|
row.type === "timeEntry" && |
|
|
|
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 |
|
|
|
return dayjs(day).isBetween(startDate, endDate, "day", "[]"); |
|
|
|
} |
|
|
|
}, |
|
|
|
[day, milestonesByProject], |
|
|
|
); |
|
|
|
|
|
|
|
const handleCancel = useCallback( |
|
|
@@ -176,6 +185,14 @@ const TimeLeaveInputTable: React.FC<Props> = ({ |
|
|
|
const editedRow = entries.find((entry) => entry.id === id); |
|
|
|
if (editedRow?._isNew) { |
|
|
|
setEntries((es) => es.filter((e) => e.id !== id)); |
|
|
|
} else { |
|
|
|
setEntries((es) => |
|
|
|
es.map((e) => |
|
|
|
e.id === id |
|
|
|
? { ...e, _error: undefined, _isPlanned: undefined } |
|
|
|
: e, |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
}, |
|
|
|
[entries], |
|
|
@@ -190,30 +207,60 @@ const TimeLeaveInputTable: React.FC<Props> = ({ |
|
|
|
|
|
|
|
const handleSave = useCallback( |
|
|
|
(id: GridRowId) => () => { |
|
|
|
if (validateRow(id)) { |
|
|
|
setRowModesModel((model) => ({ |
|
|
|
...model, |
|
|
|
[id]: { mode: GridRowModes.View }, |
|
|
|
})); |
|
|
|
} |
|
|
|
setRowModesModel((model) => ({ |
|
|
|
...model, |
|
|
|
[id]: { mode: GridRowModes.View }, |
|
|
|
})); |
|
|
|
}, |
|
|
|
[validateRow], |
|
|
|
[], |
|
|
|
); |
|
|
|
|
|
|
|
const handleEditStop = useCallback<GridEventListener<"rowEditStop">>( |
|
|
|
(params, event) => { |
|
|
|
if (!validateRow(params.id)) { |
|
|
|
event.defaultMuiPrevented = true; |
|
|
|
const processRowUpdate = useCallback( |
|
|
|
(newRow: GridRowModel<TimeLeaveRow>) => { |
|
|
|
const errors = validateRow(newRow); |
|
|
|
if (errors) { |
|
|
|
throw new ProcessRowUpdateError(newRow.id!, "validation error", errors); |
|
|
|
} |
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars |
|
|
|
const { _isNew, _error, _isPlanned, ...updatedRow } = newRow; |
|
|
|
const newIsPlanned = verifyIsPlanned(updatedRow); |
|
|
|
|
|
|
|
const rowToSave = { |
|
|
|
...updatedRow, |
|
|
|
...(updatedRow.type === "timeEntry" |
|
|
|
? { |
|
|
|
leaveTypeId: undefined, |
|
|
|
} |
|
|
|
: { |
|
|
|
projectId: undefined, |
|
|
|
taskGroupId: undefined, |
|
|
|
taskId: undefined, |
|
|
|
}), |
|
|
|
_isPlanned: newIsPlanned, |
|
|
|
} satisfies TimeLeaveRow; |
|
|
|
setEntries((es) => |
|
|
|
es.map((e) => (e.id === rowToSave.id ? rowToSave : e)), |
|
|
|
); |
|
|
|
return rowToSave; |
|
|
|
}, |
|
|
|
[validateRow], |
|
|
|
[validateRow, verifyIsPlanned], |
|
|
|
); |
|
|
|
|
|
|
|
const processRowUpdate = useCallback((newRow: GridRowModel) => { |
|
|
|
const updatedRow = { ...newRow, _isNew: false }; |
|
|
|
setEntries((es) => es.map((e) => (e.id === newRow.id ? updatedRow : e))); |
|
|
|
return updatedRow; |
|
|
|
}, []); |
|
|
|
const onProcessRowUpdateError = useCallback( |
|
|
|
(updateError: ProcessRowUpdateError) => { |
|
|
|
const errors = updateError.errors; |
|
|
|
const rowId = updateError.rowId; |
|
|
|
|
|
|
|
apiRef.current.updateRows([ |
|
|
|
{ |
|
|
|
id: rowId, |
|
|
|
_error: errors, |
|
|
|
}, |
|
|
|
]); |
|
|
|
}, |
|
|
|
[apiRef], |
|
|
|
); |
|
|
|
|
|
|
|
const columns = useMemo<GridColDef[]>( |
|
|
|
() => [ |
|
|
@@ -249,6 +296,14 @@ const TimeLeaveInputTable: React.FC<Props> = ({ |
|
|
|
]; |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
field: "projectId", |
|
|
|
editable: true, |
|
|
|
}, |
|
|
|
{ |
|
|
|
field: "leaveTypeId", |
|
|
|
editable: true, |
|
|
|
}, |
|
|
|
{ |
|
|
|
field: "type", |
|
|
|
headerName: t("Project or Leave"), |
|
|
@@ -293,23 +348,30 @@ const TimeLeaveInputTable: React.FC<Props> = ({ |
|
|
|
value: isLeave ? "leaveEntry" : "timeEntry", |
|
|
|
}); |
|
|
|
|
|
|
|
params.api.updateRows([ |
|
|
|
{ |
|
|
|
id: params.id, |
|
|
|
...(isLeave |
|
|
|
? { |
|
|
|
type: "leaveEntry", |
|
|
|
leaveTypeId: projectOrLeaveId, |
|
|
|
projectId: undefined, |
|
|
|
} |
|
|
|
: { |
|
|
|
type: "timeEntry", |
|
|
|
projectId: projectOrLeaveId, |
|
|
|
leaveTypeId: undefined, |
|
|
|
}), |
|
|
|
_error: undefined, |
|
|
|
}, |
|
|
|
]); |
|
|
|
await params.api.setEditCellValue({ |
|
|
|
id: params.id, |
|
|
|
field: isLeave ? "leaveTypeId" : "projectId", |
|
|
|
value: projectOrLeaveId, |
|
|
|
}); |
|
|
|
|
|
|
|
await params.api.setEditCellValue({ |
|
|
|
id: params.id, |
|
|
|
field: isLeave ? "projectId" : "leaveTypeId", |
|
|
|
value: undefined, |
|
|
|
}); |
|
|
|
|
|
|
|
await params.api.setEditCellValue({ |
|
|
|
id: params.id, |
|
|
|
field: "taskGroupId", |
|
|
|
value: undefined, |
|
|
|
}); |
|
|
|
|
|
|
|
await params.api.setEditCellValue({ |
|
|
|
id: params.id, |
|
|
|
field: "taskId", |
|
|
|
value: undefined, |
|
|
|
}); |
|
|
|
|
|
|
|
params.api.setCellFocus( |
|
|
|
params.id, |
|
|
|
isLeave || !projectOrLeaveId ? "inputHours" : "taskGroupId", |
|
|
@@ -332,12 +394,17 @@ const TimeLeaveInputTable: React.FC<Props> = ({ |
|
|
|
projectId={params.row.projectId} |
|
|
|
value={params.value} |
|
|
|
taskGroupsByProject={taskGroupsByProject} |
|
|
|
onTaskGroupSelect={(taskGroupId) => { |
|
|
|
params.api.setEditCellValue({ |
|
|
|
onTaskGroupSelect={async (taskGroupId) => { |
|
|
|
await params.api.setEditCellValue({ |
|
|
|
id: params.id, |
|
|
|
field: params.field, |
|
|
|
value: taskGroupId, |
|
|
|
}); |
|
|
|
await params.api.setEditCellValue({ |
|
|
|
id: params.id, |
|
|
|
field: "taskId", |
|
|
|
value: undefined, |
|
|
|
}); |
|
|
|
params.api.setCellFocus(params.id, "taskId"); |
|
|
|
}} |
|
|
|
/> |
|
|
@@ -346,12 +413,17 @@ const TimeLeaveInputTable: React.FC<Props> = ({ |
|
|
|
return ( |
|
|
|
<TaskGroupSelectWithoutProject |
|
|
|
value={params.value} |
|
|
|
onTaskGroupSelect={(taskGroupId) => { |
|
|
|
params.api.setEditCellValue({ |
|
|
|
onTaskGroupSelect={async (taskGroupId) => { |
|
|
|
await params.api.setEditCellValue({ |
|
|
|
id: params.id, |
|
|
|
field: params.field, |
|
|
|
value: taskGroupId, |
|
|
|
}); |
|
|
|
await params.api.setEditCellValue({ |
|
|
|
id: params.id, |
|
|
|
field: "taskId", |
|
|
|
value: undefined, |
|
|
|
}); |
|
|
|
params.api.setCellFocus(params.id, "taskId"); |
|
|
|
}} |
|
|
|
taskGroups={taskGroupsWithoutProject} |
|
|
@@ -614,11 +686,12 @@ const TimeLeaveInputTable: React.FC<Props> = ({ |
|
|
|
}} |
|
|
|
disableColumnMenu |
|
|
|
editMode="row" |
|
|
|
columnVisibilityModel={{ projectId: false, leaveTypeId: false }} |
|
|
|
rows={entries} |
|
|
|
rowModesModel={rowModesModel} |
|
|
|
onRowModesModelChange={setRowModesModel} |
|
|
|
onRowEditStop={handleEditStop} |
|
|
|
processRowUpdate={processRowUpdate} |
|
|
|
onProcessRowUpdateError={onProcessRowUpdateError} |
|
|
|
columns={columns} |
|
|
|
getCellClassName={(params: GridCellParams<TimeLeaveRow>) => { |
|
|
|
let classname = ""; |
|
|
|