|
@@ -1,535 +0,0 @@ |
|
|
import { Add, Check, Close, Delete } from "@mui/icons-material"; |
|
|
|
|
|
import { Box, Button, Tooltip, Typography } from "@mui/material"; |
|
|
|
|
|
import { |
|
|
|
|
|
FooterPropsOverrides, |
|
|
|
|
|
GridActionsCellItem, |
|
|
|
|
|
GridCellParams, |
|
|
|
|
|
GridColDef, |
|
|
|
|
|
GridEditInputCell, |
|
|
|
|
|
GridEventListener, |
|
|
|
|
|
GridRenderEditCellParams, |
|
|
|
|
|
GridRowId, |
|
|
|
|
|
GridRowModel, |
|
|
|
|
|
GridRowModes, |
|
|
|
|
|
GridRowModesModel, |
|
|
|
|
|
GridToolbarContainer, |
|
|
|
|
|
useGridApiRef, |
|
|
|
|
|
} from "@mui/x-data-grid"; |
|
|
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
|
|
import StyledDataGrid from "../StyledDataGrid"; |
|
|
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from "react"; |
|
|
|
|
|
import { useFormContext } from "react-hook-form"; |
|
|
|
|
|
import { RecordTimesheetInput, TimeEntry } from "@/app/api/timesheets/actions"; |
|
|
|
|
|
import { manhourFormatter } from "@/app/utils/formatUtil"; |
|
|
|
|
|
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; |
|
|
|
|
|
import uniqBy from "lodash/uniqBy"; |
|
|
|
|
|
import { TaskGroup } from "@/app/api/tasks"; |
|
|
|
|
|
import dayjs from "dayjs"; |
|
|
|
|
|
import isBetween from "dayjs/plugin/isBetween"; |
|
|
|
|
|
import ProjectSelect from "./ProjectSelect"; |
|
|
|
|
|
import TaskGroupSelect from "./TaskGroupSelect"; |
|
|
|
|
|
import TaskSelect from "./TaskSelect"; |
|
|
|
|
|
import { |
|
|
|
|
|
DAILY_NORMAL_MAX_HOURS, |
|
|
|
|
|
TimeEntryError, |
|
|
|
|
|
validateTimeEntry, |
|
|
|
|
|
} from "@/app/api/timesheets/utils"; |
|
|
|
|
|
import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; |
|
|
|
|
|
import FastTimeEntryModal from "./FastTimeEntryModal"; |
|
|
|
|
|
|
|
|
|
|
|
dayjs.extend(isBetween); |
|
|
|
|
|
|
|
|
|
|
|
interface Props { |
|
|
|
|
|
day: string; |
|
|
|
|
|
isHoliday: boolean; |
|
|
|
|
|
allProjects: ProjectWithTasks[]; |
|
|
|
|
|
assignedProjects: AssignedProject[]; |
|
|
|
|
|
fastEntryEnabled?: boolean; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export type TimeEntryRow = Partial< |
|
|
|
|
|
TimeEntry & { |
|
|
|
|
|
_isNew: boolean; |
|
|
|
|
|
_error: TimeEntryError; |
|
|
|
|
|
isPlanned?: boolean; |
|
|
|
|
|
} |
|
|
|
|
|
>; |
|
|
|
|
|
|
|
|
|
|
|
const EntryInputTable: React.FC<Props> = ({ |
|
|
|
|
|
day, |
|
|
|
|
|
allProjects, |
|
|
|
|
|
assignedProjects, |
|
|
|
|
|
isHoliday, |
|
|
|
|
|
fastEntryEnabled, |
|
|
|
|
|
}) => { |
|
|
|
|
|
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]); |
|
|
|
|
|
|
|
|
|
|
|
// To check for start / end planned dates |
|
|
|
|
|
const milestonesByProject = useMemo(() => { |
|
|
|
|
|
return assignedProjects.reduce<{ |
|
|
|
|
|
[projectId: AssignedProject["id"]]: AssignedProject["milestones"]; |
|
|
|
|
|
}>((acc, project) => { |
|
|
|
|
|
return { ...acc, [project.id]: { ...project.milestones } }; |
|
|
|
|
|
}, {}); |
|
|
|
|
|
}, [assignedProjects]); |
|
|
|
|
|
|
|
|
|
|
|
const { getValues, setValue, clearErrors } = |
|
|
|
|
|
useFormContext<RecordTimesheetInput>(); |
|
|
|
|
|
const currentEntries = getValues(day); |
|
|
|
|
|
|
|
|
|
|
|
const [entries, setEntries] = useState<TimeEntryRow[]>(currentEntries || []); |
|
|
|
|
|
|
|
|
|
|
|
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); |
|
|
|
|
|
|
|
|
|
|
|
const apiRef = useGridApiRef(); |
|
|
|
|
|
const addRow = useCallback(() => { |
|
|
|
|
|
const id = Date.now(); |
|
|
|
|
|
setEntries((e) => [...e, { id, _isNew: true }]); |
|
|
|
|
|
setRowModesModel((model) => ({ |
|
|
|
|
|
...model, |
|
|
|
|
|
[id]: { mode: GridRowModes.Edit, fieldToFocus: "projectId" }, |
|
|
|
|
|
})); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const validateRow = useCallback( |
|
|
|
|
|
(id: GridRowId) => { |
|
|
|
|
|
const row = apiRef.current.getRowWithUpdatedValues( |
|
|
|
|
|
id, |
|
|
|
|
|
"", |
|
|
|
|
|
) as TimeEntryRow; |
|
|
|
|
|
|
|
|
|
|
|
const error = validateTimeEntry(row, isHoliday); |
|
|
|
|
|
|
|
|
|
|
|
// Test for warnings |
|
|
|
|
|
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; |
|
|
|
|
|
}, |
|
|
|
|
|
[apiRef, day, isHoliday, milestonesByProject], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const handleCancel = useCallback( |
|
|
|
|
|
(id: GridRowId) => () => { |
|
|
|
|
|
setRowModesModel((model) => ({ |
|
|
|
|
|
...model, |
|
|
|
|
|
[id]: { mode: GridRowModes.View, ignoreModifications: true }, |
|
|
|
|
|
})); |
|
|
|
|
|
const editedRow = entries.find((entry) => entry.id === id); |
|
|
|
|
|
if (editedRow?._isNew) { |
|
|
|
|
|
setEntries((es) => es.filter((e) => e.id !== id)); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
[entries], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const handleDelete = useCallback( |
|
|
|
|
|
(id: GridRowId) => () => { |
|
|
|
|
|
setEntries((es) => es.filter((e) => e.id !== id)); |
|
|
|
|
|
}, |
|
|
|
|
|
[], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const handleSave = useCallback( |
|
|
|
|
|
(id: GridRowId) => () => { |
|
|
|
|
|
if (validateRow(id)) { |
|
|
|
|
|
setRowModesModel((model) => ({ |
|
|
|
|
|
...model, |
|
|
|
|
|
[id]: { mode: GridRowModes.View }, |
|
|
|
|
|
})); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
[validateRow], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const handleEditStop = useCallback<GridEventListener<"rowEditStop">>( |
|
|
|
|
|
(params, event) => { |
|
|
|
|
|
if (!validateRow(params.id)) { |
|
|
|
|
|
event.defaultMuiPrevented = true; |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
[validateRow], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const processRowUpdate = useCallback((newRow: GridRowModel) => { |
|
|
|
|
|
const updatedRow = { ...newRow, _isNew: false }; |
|
|
|
|
|
setEntries((es) => es.map((e) => (e.id === newRow.id ? updatedRow : e))); |
|
|
|
|
|
return updatedRow; |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const columns = useMemo<GridColDef[]>( |
|
|
|
|
|
() => [ |
|
|
|
|
|
{ |
|
|
|
|
|
type: "actions", |
|
|
|
|
|
field: "actions", |
|
|
|
|
|
headerName: t("Actions"), |
|
|
|
|
|
getActions: ({ id }) => { |
|
|
|
|
|
if (rowModesModel[id]?.mode === GridRowModes.Edit) { |
|
|
|
|
|
return [ |
|
|
|
|
|
<GridActionsCellItem |
|
|
|
|
|
key="accpet-action" |
|
|
|
|
|
icon={<Check />} |
|
|
|
|
|
label={t("Save")} |
|
|
|
|
|
onClick={handleSave(id)} |
|
|
|
|
|
/>, |
|
|
|
|
|
<GridActionsCellItem |
|
|
|
|
|
key="cancel-action" |
|
|
|
|
|
icon={<Close />} |
|
|
|
|
|
label={t("Cancel")} |
|
|
|
|
|
onClick={handleCancel(id)} |
|
|
|
|
|
/>, |
|
|
|
|
|
]; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return [ |
|
|
|
|
|
<GridActionsCellItem |
|
|
|
|
|
key="delete-action" |
|
|
|
|
|
icon={<Delete />} |
|
|
|
|
|
label={t("Remove")} |
|
|
|
|
|
onClick={handleDelete(id)} |
|
|
|
|
|
/>, |
|
|
|
|
|
]; |
|
|
|
|
|
}, |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
field: "projectId", |
|
|
|
|
|
headerName: t("Project Code and Name"), |
|
|
|
|
|
width: 300, |
|
|
|
|
|
editable: true, |
|
|
|
|
|
valueFormatter(params) { |
|
|
|
|
|
const project = allProjects.find((p) => p.id === params.value); |
|
|
|
|
|
return project ? `${project.code} - ${project.name}` : t("None"); |
|
|
|
|
|
}, |
|
|
|
|
|
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow, number>) { |
|
|
|
|
|
return ( |
|
|
|
|
|
<ProjectSelect |
|
|
|
|
|
multiple={false} |
|
|
|
|
|
allProjects={allProjects} |
|
|
|
|
|
assignedProjects={assignedProjects} |
|
|
|
|
|
value={params.value} |
|
|
|
|
|
onProjectSelect={(projectId) => { |
|
|
|
|
|
params.api.setEditCellValue({ |
|
|
|
|
|
id: params.id, |
|
|
|
|
|
field: params.field, |
|
|
|
|
|
value: projectId, |
|
|
|
|
|
}); |
|
|
|
|
|
params.api.setCellFocus(params.id, "taskGroupId"); |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
field: "taskGroupId", |
|
|
|
|
|
headerName: t("Stage"), |
|
|
|
|
|
width: 200, |
|
|
|
|
|
editable: true, |
|
|
|
|
|
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow, number>) { |
|
|
|
|
|
return ( |
|
|
|
|
|
<TaskGroupSelect |
|
|
|
|
|
projectId={params.row.projectId} |
|
|
|
|
|
value={params.value} |
|
|
|
|
|
taskGroupsByProject={taskGroupsByProject} |
|
|
|
|
|
onTaskGroupSelect={(taskGroupId) => { |
|
|
|
|
|
params.api.setEditCellValue({ |
|
|
|
|
|
id: params.id, |
|
|
|
|
|
field: params.field, |
|
|
|
|
|
value: taskGroupId, |
|
|
|
|
|
}); |
|
|
|
|
|
params.api.setCellFocus(params.id, "taskId"); |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
valueFormatter(params) { |
|
|
|
|
|
const taskGroups = params.id |
|
|
|
|
|
? taskGroupsByProject[params.api.getRow(params.id).projectId] || [] |
|
|
|
|
|
: []; |
|
|
|
|
|
const taskGroup = taskGroups.find((tg) => tg.value === params.value); |
|
|
|
|
|
return taskGroup ? taskGroup.label : t("None"); |
|
|
|
|
|
}, |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
field: "taskId", |
|
|
|
|
|
headerName: t("Task"), |
|
|
|
|
|
width: 200, |
|
|
|
|
|
editable: true, |
|
|
|
|
|
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow, number>) { |
|
|
|
|
|
return ( |
|
|
|
|
|
<TaskSelect |
|
|
|
|
|
value={params.value} |
|
|
|
|
|
projectId={params.row.projectId} |
|
|
|
|
|
taskGroupId={params.row.taskGroupId} |
|
|
|
|
|
allProjects={allProjects} |
|
|
|
|
|
onTaskSelect={(taskId) => { |
|
|
|
|
|
params.api.setEditCellValue({ |
|
|
|
|
|
id: params.id, |
|
|
|
|
|
field: params.field, |
|
|
|
|
|
value: taskId, |
|
|
|
|
|
}); |
|
|
|
|
|
params.api.setCellFocus(params.id, "inputHours"); |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
valueFormatter(params) { |
|
|
|
|
|
const projectId = params.id |
|
|
|
|
|
? params.api.getRow(params.id).projectId |
|
|
|
|
|
: undefined; |
|
|
|
|
|
|
|
|
|
|
|
const task = projectId |
|
|
|
|
|
? allProjects |
|
|
|
|
|
.find((p) => p.id === projectId) |
|
|
|
|
|
?.tasks.find((t) => t.id === params.value) |
|
|
|
|
|
: undefined; |
|
|
|
|
|
|
|
|
|
|
|
return task ? task.name : t("None"); |
|
|
|
|
|
}, |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
field: "inputHours", |
|
|
|
|
|
headerName: t("Hours"), |
|
|
|
|
|
width: 100, |
|
|
|
|
|
editable: true, |
|
|
|
|
|
type: "number", |
|
|
|
|
|
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow>) { |
|
|
|
|
|
const errorMessage = |
|
|
|
|
|
params.row._error?.[params.field as keyof TimeEntry]; |
|
|
|
|
|
const content = <GridEditInputCell {...params} />; |
|
|
|
|
|
return errorMessage ? ( |
|
|
|
|
|
<Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}> |
|
|
|
|
|
<Box width="100%">{content}</Box> |
|
|
|
|
|
</Tooltip> |
|
|
|
|
|
) : ( |
|
|
|
|
|
content |
|
|
|
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
valueParser(value) { |
|
|
|
|
|
return value ? roundToNearestQuarter(value) : value; |
|
|
|
|
|
}, |
|
|
|
|
|
valueFormatter(params) { |
|
|
|
|
|
return manhourFormatter.format(params.value || 0); |
|
|
|
|
|
}, |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
field: "otHours", |
|
|
|
|
|
headerName: t("Other Hours"), |
|
|
|
|
|
width: 150, |
|
|
|
|
|
editable: true, |
|
|
|
|
|
type: "number", |
|
|
|
|
|
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow>) { |
|
|
|
|
|
const errorMessage = |
|
|
|
|
|
params.row._error?.[params.field as keyof TimeEntry]; |
|
|
|
|
|
const content = <GridEditInputCell {...params} />; |
|
|
|
|
|
return errorMessage ? ( |
|
|
|
|
|
<Tooltip title={t(errorMessage)}> |
|
|
|
|
|
<Box width="100%">{content}</Box> |
|
|
|
|
|
</Tooltip> |
|
|
|
|
|
) : ( |
|
|
|
|
|
content |
|
|
|
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
valueParser(value) { |
|
|
|
|
|
return value ? roundToNearestQuarter(value) : value; |
|
|
|
|
|
}, |
|
|
|
|
|
valueFormatter(params) { |
|
|
|
|
|
return manhourFormatter.format(params.value || 0); |
|
|
|
|
|
}, |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
field: "remark", |
|
|
|
|
|
headerName: t("Remark"), |
|
|
|
|
|
sortable: false, |
|
|
|
|
|
flex: 1, |
|
|
|
|
|
editable: true, |
|
|
|
|
|
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow>) { |
|
|
|
|
|
const errorMessage = |
|
|
|
|
|
params.row._error?.[params.field as keyof TimeEntry]; |
|
|
|
|
|
const content = <GridEditInputCell {...params} />; |
|
|
|
|
|
return errorMessage ? ( |
|
|
|
|
|
<Tooltip title={t(errorMessage)}> |
|
|
|
|
|
<Box width="100%">{content}</Box> |
|
|
|
|
|
</Tooltip> |
|
|
|
|
|
) : ( |
|
|
|
|
|
content |
|
|
|
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
}, |
|
|
|
|
|
], |
|
|
|
|
|
[ |
|
|
|
|
|
t, |
|
|
|
|
|
rowModesModel, |
|
|
|
|
|
handleDelete, |
|
|
|
|
|
handleSave, |
|
|
|
|
|
handleCancel, |
|
|
|
|
|
assignedProjects, |
|
|
|
|
|
allProjects, |
|
|
|
|
|
taskGroupsByProject, |
|
|
|
|
|
], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
setValue(day, [ |
|
|
|
|
|
...entries |
|
|
|
|
|
.filter((e) => !e._isNew && !e._error && e.id) |
|
|
|
|
|
.map(({ isPlanned, _error, _isNew, ...entry }) => ({ |
|
|
|
|
|
id: entry.id!, |
|
|
|
|
|
...entry, |
|
|
|
|
|
})), |
|
|
|
|
|
]); |
|
|
|
|
|
clearErrors(day); |
|
|
|
|
|
}, [getValues, entries, setValue, day, clearErrors]); |
|
|
|
|
|
|
|
|
|
|
|
const hasOutOfPlannedStages = entries.some( |
|
|
|
|
|
(entry) => entry.isPlanned !== undefined && !entry.isPlanned, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// Fast entry modal |
|
|
|
|
|
const [fastEntryModalOpen, setFastEntryModalOpen] = useState(false); |
|
|
|
|
|
const closeFastEntryModal = useCallback(() => { |
|
|
|
|
|
setFastEntryModalOpen(false); |
|
|
|
|
|
}, []); |
|
|
|
|
|
const openFastEntryModal = useCallback(() => { |
|
|
|
|
|
setFastEntryModalOpen(true); |
|
|
|
|
|
}, []); |
|
|
|
|
|
const onSaveFastEntry = useCallback(async (entries: TimeEntry[]) => { |
|
|
|
|
|
setEntries((e) => [...e, ...entries]); |
|
|
|
|
|
setFastEntryModalOpen(false); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const footer = ( |
|
|
|
|
|
<Box display="flex" gap={2} alignItems="center"> |
|
|
|
|
|
<Button |
|
|
|
|
|
disableRipple |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
startIcon={<Add />} |
|
|
|
|
|
onClick={addRow} |
|
|
|
|
|
size="small" |
|
|
|
|
|
> |
|
|
|
|
|
{t("Record time")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
{fastEntryEnabled && <Button |
|
|
|
|
|
disableRipple |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
startIcon={<Add />} |
|
|
|
|
|
onClick={openFastEntryModal} |
|
|
|
|
|
size="small" |
|
|
|
|
|
> |
|
|
|
|
|
{t("Fast time entry")} |
|
|
|
|
|
</Button>} |
|
|
|
|
|
{hasOutOfPlannedStages && ( |
|
|
|
|
|
<Typography color="warning.main" variant="body2"> |
|
|
|
|
|
{t("There are entries for stages out of planned dates!")} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
)} |
|
|
|
|
|
</Box> |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
<> |
|
|
|
|
|
<StyledDataGrid |
|
|
|
|
|
apiRef={apiRef} |
|
|
|
|
|
autoHeight |
|
|
|
|
|
sx={{ |
|
|
|
|
|
"--DataGrid-overlayHeight": "100px", |
|
|
|
|
|
".MuiDataGrid-row .MuiDataGrid-cell.hasError": { |
|
|
|
|
|
border: "1px solid", |
|
|
|
|
|
borderColor: "error.main", |
|
|
|
|
|
}, |
|
|
|
|
|
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { |
|
|
|
|
|
border: "1px solid", |
|
|
|
|
|
borderColor: "warning.main", |
|
|
|
|
|
}, |
|
|
|
|
|
}} |
|
|
|
|
|
disableColumnMenu |
|
|
|
|
|
editMode="row" |
|
|
|
|
|
rows={entries} |
|
|
|
|
|
rowModesModel={rowModesModel} |
|
|
|
|
|
onRowModesModelChange={setRowModesModel} |
|
|
|
|
|
onRowEditStop={handleEditStop} |
|
|
|
|
|
processRowUpdate={processRowUpdate} |
|
|
|
|
|
columns={columns} |
|
|
|
|
|
getCellClassName={(params: GridCellParams<TimeEntryRow>) => { |
|
|
|
|
|
let classname = ""; |
|
|
|
|
|
if (params.row._error?.[params.field as keyof TimeEntry]) { |
|
|
|
|
|
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: { child: footer }, |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
{fastEntryEnabled && ( |
|
|
|
|
|
<FastTimeEntryModal |
|
|
|
|
|
allProjects={allProjects} |
|
|
|
|
|
assignedProjects={assignedProjects} |
|
|
|
|
|
open={fastEntryModalOpen} |
|
|
|
|
|
isHoliday={Boolean(isHoliday)} |
|
|
|
|
|
onClose={closeFastEntryModal} |
|
|
|
|
|
onSave={onSaveFastEntry} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
</> |
|
|
|
|
|
); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const NoRowsOverlay: React.FC = () => { |
|
|
|
|
|
const { t } = useTranslation("home"); |
|
|
|
|
|
return ( |
|
|
|
|
|
<Box |
|
|
|
|
|
display="flex" |
|
|
|
|
|
justifyContent="center" |
|
|
|
|
|
alignItems="center" |
|
|
|
|
|
height="100%" |
|
|
|
|
|
> |
|
|
|
|
|
<Typography variant="caption">{t("Add some time entries!")}</Typography> |
|
|
|
|
|
</Box> |
|
|
|
|
|
); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { |
|
|
|
|
|
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
export default EntryInputTable; |
|
|
|