@@ -57,8 +57,9 @@ const LeaveEditModal: React.FC<Props> = ({ | |||
const valid = await trigger(); | |||
if (valid) { | |||
onSave(getValues()); | |||
reset(); | |||
} | |||
}, [getValues, onSave, trigger]); | |||
}, [getValues, onSave, reset, trigger]); | |||
const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
(...args) => { | |||
@@ -86,93 +86,100 @@ const MobileLeaveEntry: React.FC<Props> = ({ date, leaveTypes }) => { | |||
); | |||
return ( | |||
<Box | |||
marginInline={2} | |||
flex={1} | |||
display="flex" | |||
flexDirection="column" | |||
gap={2} | |||
> | |||
<> | |||
<Typography | |||
paddingInline={2} | |||
variant="overline" | |||
color={dayJsObj.day() === 0 ? "error.main" : undefined} | |||
> | |||
{shortDateFormatter(language).format(dayJsObj.toDate())} | |||
</Typography> | |||
{currentEntries.length ? ( | |||
currentEntries.map((entry, index) => { | |||
return ( | |||
<Card key={`${entry.id}-${index}`} sx={{ marginInline: 1 }}> | |||
<CardContent | |||
sx={{ | |||
padding: 2, | |||
display: "flex", | |||
flexDirection: "column", | |||
gap: 2, | |||
"&:last-child": { | |||
paddingBottom: 2, | |||
}, | |||
}} | |||
<Box | |||
paddingInline={2} | |||
flex={1} | |||
display="flex" | |||
flexDirection="column" | |||
gap={2} | |||
overflow="scroll" | |||
> | |||
{currentEntries.length ? ( | |||
currentEntries.map((entry, index) => { | |||
return ( | |||
<Card | |||
key={`${entry.id}-${index}`} | |||
sx={{ marginInline: 1, overflow: "visible" }} | |||
> | |||
<Box | |||
display="flex" | |||
justifyContent="space-between" | |||
alignItems="flex-start" | |||
<CardContent | |||
sx={{ | |||
padding: 2, | |||
display: "flex", | |||
flexDirection: "column", | |||
gap: 2, | |||
"&:last-child": { | |||
paddingBottom: 2, | |||
}, | |||
}} | |||
> | |||
<Box> | |||
<Typography | |||
variant="body2" | |||
component="div" | |||
fontWeight="bold" | |||
> | |||
{leaveTypeMap[entry.leaveTypeId].name} | |||
</Typography> | |||
<Typography component="p"> | |||
{manhourFormatter.format(entry.inputHours)} | |||
</Typography> | |||
</Box> | |||
<IconButton | |||
size="small" | |||
color="primary" | |||
onClick={openEditModal(entry)} | |||
<Box | |||
display="flex" | |||
justifyContent="space-between" | |||
alignItems="flex-start" | |||
> | |||
<Edit /> | |||
</IconButton> | |||
</Box> | |||
{entry.remark && ( | |||
<Box> | |||
<Typography | |||
variant="body2" | |||
component="div" | |||
fontWeight="bold" | |||
<Box> | |||
<Typography | |||
variant="body2" | |||
component="div" | |||
fontWeight="bold" | |||
> | |||
{leaveTypeMap[entry.leaveTypeId].name} | |||
</Typography> | |||
<Typography component="p"> | |||
{manhourFormatter.format(entry.inputHours)} | |||
</Typography> | |||
</Box> | |||
<IconButton | |||
size="small" | |||
color="primary" | |||
onClick={openEditModal(entry)} | |||
> | |||
{t("Remark")} | |||
</Typography> | |||
<Typography component="p">{entry.remark}</Typography> | |||
<Edit /> | |||
</IconButton> | |||
</Box> | |||
)} | |||
</CardContent> | |||
</Card> | |||
); | |||
}) | |||
) : ( | |||
<Typography variant="body2" display="block"> | |||
{t("Add some leave entries!")} | |||
</Typography> | |||
)} | |||
<Box> | |||
<Button startIcon={<Add />} onClick={openEditModal()}> | |||
{t("Record leave")} | |||
</Button> | |||
{entry.remark && ( | |||
<Box> | |||
<Typography | |||
variant="body2" | |||
component="div" | |||
fontWeight="bold" | |||
> | |||
{t("Remark")} | |||
</Typography> | |||
<Typography component="p">{entry.remark}</Typography> | |||
</Box> | |||
)} | |||
</CardContent> | |||
</Card> | |||
); | |||
}) | |||
) : ( | |||
<Typography variant="body2" display="block"> | |||
{t("Add some leave entries!")} | |||
</Typography> | |||
)} | |||
<Box> | |||
<Button startIcon={<Add />} onClick={openEditModal()}> | |||
{t("Record leave")} | |||
</Button> | |||
</Box> | |||
<LeaveEditModal | |||
leaveTypes={leaveTypes} | |||
open={editModalOpen} | |||
onClose={closeEditModal} | |||
onSave={onSaveEntry} | |||
{...editModalProps} | |||
/> | |||
</Box> | |||
<LeaveEditModal | |||
leaveTypes={leaveTypes} | |||
open={editModalOpen} | |||
onClose={closeEditModal} | |||
onSave={onSaveEntry} | |||
{...editModalProps} | |||
/> | |||
</Box> | |||
</> | |||
); | |||
}; | |||
@@ -277,7 +277,6 @@ const EntryInputTable: React.FC<Props> = ({ | |||
projectId={params.row.projectId} | |||
taskGroupId={params.row.taskGroupId} | |||
allProjects={allProjects} | |||
editCellProps={params} | |||
onTaskSelect={(taskId) => { | |||
params.api.setEditCellValue({ | |||
id: params.id, | |||
@@ -10,10 +10,13 @@ import { | |||
Typography, | |||
} from "@mui/material"; | |||
import dayjs from "dayjs"; | |||
import React from "react"; | |||
import React, { useCallback, useMemo, useState } from "react"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { useTranslation } from "react-i18next"; | |||
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | |||
import TimesheetEditModal, { | |||
Props as TimesheetEditModalProps, | |||
} from "./TimesheetEditModal"; | |||
interface Props { | |||
date: string; | |||
@@ -30,11 +33,200 @@ const MobileTimesheetEntry: React.FC<Props> = ({ | |||
t, | |||
i18n: { language }, | |||
} = useTranslation("home"); | |||
const projectMap = useMemo(() => { | |||
return allProjects.reduce<{ | |||
[id: ProjectWithTasks["id"]]: ProjectWithTasks; | |||
}>((acc, project) => { | |||
return { ...acc, [project.id]: project }; | |||
}, {}); | |||
}, [allProjects]); | |||
const dayJsObj = dayjs(date); | |||
const { watch, setValue } = useFormContext<RecordTimesheetInput>(); | |||
const currentEntries = watch(date); | |||
return null; | |||
// Edit modal | |||
const [editModalProps, setEditModalProps] = useState< | |||
Partial<TimesheetEditModalProps> | |||
>({}); | |||
const [editModalOpen, setEditModalOpen] = useState(false); | |||
const openEditModal = useCallback( | |||
(defaultValues?: TimeEntry) => () => { | |||
setEditModalProps({ | |||
defaultValues, | |||
onDelete: defaultValues | |||
? () => { | |||
setValue( | |||
date, | |||
currentEntries.filter((entry) => entry.id !== defaultValues.id), | |||
); | |||
setEditModalOpen(false); | |||
} | |||
: undefined, | |||
}); | |||
setEditModalOpen(true); | |||
}, | |||
[currentEntries, date, setValue], | |||
); | |||
const closeEditModal = useCallback(() => { | |||
setEditModalOpen(false); | |||
}, []); | |||
const onSaveEntry = useCallback( | |||
(entry: TimeEntry) => { | |||
const existingEntry = currentEntries.find((e) => e.id === entry.id); | |||
if (existingEntry) { | |||
setValue( | |||
date, | |||
currentEntries.map((e) => ({ | |||
...(e.id === existingEntry.id ? entry : e), | |||
})), | |||
); | |||
} else { | |||
setValue(date, [...currentEntries, entry]); | |||
} | |||
setEditModalOpen(false); | |||
}, | |||
[currentEntries, date, setValue], | |||
); | |||
return ( | |||
<> | |||
<Typography | |||
paddingInline={2} | |||
variant="overline" | |||
color={dayJsObj.day() === 0 ? "error.main" : undefined} | |||
> | |||
{shortDateFormatter(language).format(dayJsObj.toDate())} | |||
</Typography> | |||
<Box | |||
paddingInline={2} | |||
flex={1} | |||
display="flex" | |||
flexDirection="column" | |||
gap={2} | |||
overflow="scroll" | |||
> | |||
{currentEntries.length ? ( | |||
currentEntries.map((entry, index) => { | |||
const project = entry.projectId | |||
? projectMap[entry.projectId] | |||
: undefined; | |||
const task = project?.tasks.find((t) => t.id === entry.taskId); | |||
return ( | |||
<Card | |||
key={`${entry.id}-${index}`} | |||
sx={{ marginInline: 1, overflow: "visible" }} | |||
> | |||
<CardContent | |||
sx={{ | |||
padding: 2, | |||
display: "flex", | |||
flexDirection: "column", | |||
gap: 2, | |||
"&:last-child": { | |||
paddingBottom: 2, | |||
}, | |||
}} | |||
> | |||
<Box | |||
display="flex" | |||
justifyContent="space-between" | |||
alignItems="flex-start" | |||
gap={2} | |||
> | |||
<Box> | |||
<Typography | |||
variant="body2" | |||
component="div" | |||
fontWeight="bold" | |||
> | |||
{project | |||
? `${project.code} - ${project.name}` | |||
: t("Non-billable Task")} | |||
</Typography> | |||
{task && ( | |||
<Typography variant="body2" component="div"> | |||
{task.name} | |||
</Typography> | |||
)} | |||
</Box> | |||
<IconButton | |||
size="small" | |||
color="primary" | |||
onClick={openEditModal(entry)} | |||
> | |||
<Edit /> | |||
</IconButton> | |||
</Box> | |||
<Box display="flex" gap={2}> | |||
<Box> | |||
<Typography | |||
variant="body2" | |||
component="div" | |||
fontWeight="bold" | |||
> | |||
{t("Hours")} | |||
</Typography> | |||
<Typography component="p"> | |||
{manhourFormatter.format(entry.inputHours || 0)} | |||
</Typography> | |||
</Box> | |||
<Box> | |||
<Typography | |||
variant="body2" | |||
component="div" | |||
fontWeight="bold" | |||
> | |||
{t("Other Hours")} | |||
</Typography> | |||
<Typography component="p"> | |||
{manhourFormatter.format(entry.otHours || 0)} | |||
</Typography> | |||
</Box> | |||
</Box> | |||
{entry.remark && ( | |||
<Box> | |||
<Typography | |||
variant="body2" | |||
component="div" | |||
fontWeight="bold" | |||
> | |||
{t("Remark")} | |||
</Typography> | |||
<Typography component="p">{entry.remark}</Typography> | |||
</Box> | |||
)} | |||
</CardContent> | |||
</Card> | |||
); | |||
}) | |||
) : ( | |||
<Typography variant="body2" display="block"> | |||
{t("Add some time entries!")} | |||
</Typography> | |||
)} | |||
<Box> | |||
<Button startIcon={<Add />} onClick={openEditModal()}> | |||
{t("Record leave")} | |||
</Button> | |||
</Box> | |||
<TimesheetEditModal | |||
allProjects={allProjects} | |||
assignedProjects={assignedProjects} | |||
open={editModalOpen} | |||
onClose={closeEditModal} | |||
onSave={onSaveEntry} | |||
{...editModalProps} | |||
/> | |||
</Box> | |||
</> | |||
); | |||
}; | |||
export default MobileTimesheetEntry; |
@@ -1,9 +1,11 @@ | |||
import React, { useCallback, useMemo } from "react"; | |||
import { | |||
Autocomplete, | |||
ListSubheader, | |||
MenuItem, | |||
Select, | |||
SelectChangeEvent, | |||
TextField, | |||
} from "@mui/material"; | |||
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | |||
import { useTranslation } from "react-i18next"; | |||
@@ -16,6 +18,49 @@ interface Props { | |||
onProjectSelect: (projectId: number | string) => void; | |||
} | |||
// const AutocompleteProjectSelect: React.FC<Props> = ({ | |||
// allProjects, | |||
// assignedProjects, | |||
// value, | |||
// onProjectSelect, | |||
// }) => { | |||
// const { t } = useTranslation("home"); | |||
// const nonAssignedProjects = useMemo(() => { | |||
// return differenceBy(allProjects, assignedProjects, "id"); | |||
// }, [allProjects, assignedProjects]); | |||
// const options = useMemo(() => { | |||
// return [ | |||
// { | |||
// value: "", | |||
// label: t("None"), | |||
// group: "non-billable", | |||
// }, | |||
// ...assignedProjects.map((p) => ({ | |||
// value: p.id, | |||
// label: `${p.code} - ${p.name}`, | |||
// group: "assigned", | |||
// })), | |||
// ...nonAssignedProjects.map((p) => ({ | |||
// value: p.id, | |||
// label: `${p.code} - ${p.name}`, | |||
// group: "non-assigned", | |||
// })), | |||
// ]; | |||
// }, [assignedProjects, nonAssignedProjects, t]); | |||
// return ( | |||
// <Autocomplete | |||
// disableClearable | |||
// fullWidth | |||
// groupBy={(option) => option.group} | |||
// getOptionLabel={(option) => option.label} | |||
// options={options} | |||
// renderInput={(params) => <TextField {...params} />} | |||
// /> | |||
// ); | |||
// }; | |||
const ProjectSelect: React.FC<Props> = ({ | |||
allProjects, | |||
assignedProjects, | |||
@@ -68,6 +113,7 @@ const ProjectSelect: React.FC<Props> = ({ | |||
<MenuItem | |||
key={project.id} | |||
value={project.id} | |||
sx={{ whiteSpace: "wrap" }} | |||
>{`${project.code} - ${project.name}`}</MenuItem> | |||
)), | |||
]} | |||
@@ -79,6 +125,7 @@ const ProjectSelect: React.FC<Props> = ({ | |||
<MenuItem | |||
key={project.id} | |||
value={project.id} | |||
sx={{ whiteSpace: "wrap" }} | |||
>{`${project.code} - ${project.name}`}</MenuItem> | |||
)), | |||
]} | |||
@@ -13,6 +13,7 @@ interface Props { | |||
projectId: number | undefined; | |||
value: number | undefined; | |||
onTaskGroupSelect: (taskGroupId: number | string) => void; | |||
error?: boolean; | |||
} | |||
const TaskGroupSelect: React.FC<Props> = ({ | |||
@@ -20,6 +21,7 @@ const TaskGroupSelect: React.FC<Props> = ({ | |||
projectId, | |||
onTaskGroupSelect, | |||
taskGroupsByProject, | |||
error, | |||
}) => { | |||
const { t } = useTranslation("home"); | |||
@@ -35,6 +37,7 @@ const TaskGroupSelect: React.FC<Props> = ({ | |||
return ( | |||
<Select | |||
error={error} | |||
displayEmpty | |||
disabled={taskGroups.length === 0} | |||
value={value || ""} | |||
@@ -58,7 +61,11 @@ const TaskGroupSelect: React.FC<Props> = ({ | |||
> | |||
{taskGroups.length === 0 && <MenuItem value={""}>{t("None")}</MenuItem>} | |||
{taskGroups.map((taskGroup) => ( | |||
<MenuItem key={taskGroup.value} value={taskGroup.value}> | |||
<MenuItem | |||
key={taskGroup.value} | |||
value={taskGroup.value} | |||
sx={{ whiteSpace: "wrap" }} | |||
> | |||
{taskGroup.label} | |||
</MenuItem> | |||
))} | |||
@@ -1,7 +1,5 @@ | |||
import React, { useCallback } from "react"; | |||
import { MenuItem, Select, SelectChangeEvent } from "@mui/material"; | |||
import { GridRenderEditCellParams } from "@mui/x-data-grid"; | |||
import { TimeEntryRow } from "./EntryInputTable"; | |||
import { useTranslation } from "react-i18next"; | |||
import { ProjectWithTasks } from "@/app/api/projects"; | |||
@@ -10,8 +8,8 @@ interface Props { | |||
value: number | undefined; | |||
projectId: number | undefined; | |||
taskGroupId: number | undefined; | |||
editCellProps: GridRenderEditCellParams<TimeEntryRow, number>; | |||
onTaskSelect: (taskId: number | string) => void; | |||
error?: boolean; | |||
} | |||
const TaskSelect: React.FC<Props> = ({ | |||
@@ -20,6 +18,7 @@ const TaskSelect: React.FC<Props> = ({ | |||
projectId, | |||
taskGroupId, | |||
onTaskSelect, | |||
error | |||
}) => { | |||
const { t } = useTranslation("home"); | |||
@@ -38,6 +37,7 @@ const TaskSelect: React.FC<Props> = ({ | |||
return ( | |||
<Select | |||
error={error} | |||
displayEmpty | |||
disabled={tasks.length === 0} | |||
value={value || ""} | |||
@@ -61,7 +61,7 @@ const TaskSelect: React.FC<Props> = ({ | |||
> | |||
{tasks.length === 0 && <MenuItem value={""}>{t("None")}</MenuItem>} | |||
{tasks.map((task) => ( | |||
<MenuItem key={task.id} value={task.id}> | |||
<MenuItem key={task.id} value={task.id} sx={{ whiteSpace: "wrap" }}> | |||
{task.name} | |||
</MenuItem> | |||
))} | |||
@@ -0,0 +1,247 @@ | |||
import { TimeEntry } from "@/app/api/timesheets/actions"; | |||
import { Check, Delete } from "@mui/icons-material"; | |||
import { | |||
Box, | |||
Button, | |||
FormControl, | |||
InputLabel, | |||
Modal, | |||
ModalProps, | |||
Paper, | |||
SxProps, | |||
TextField, | |||
} from "@mui/material"; | |||
import React, { useCallback, useEffect, useMemo } from "react"; | |||
import { Controller, useForm } from "react-hook-form"; | |||
import { useTranslation } from "react-i18next"; | |||
import ProjectSelect from "./ProjectSelect"; | |||
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | |||
import TaskGroupSelect from "./TaskGroupSelect"; | |||
import TaskSelect from "./TaskSelect"; | |||
import { TaskGroup } from "@/app/api/tasks"; | |||
import uniqBy from "lodash/uniqBy"; | |||
export interface Props extends Omit<ModalProps, "children"> { | |||
onSave: (leaveEntry: TimeEntry) => void; | |||
onDelete?: () => void; | |||
defaultValues?: Partial<TimeEntry>; | |||
allProjects: ProjectWithTasks[]; | |||
assignedProjects: AssignedProject[]; | |||
} | |||
const modalSx: SxProps = { | |||
position: "absolute", | |||
top: "50%", | |||
left: "50%", | |||
transform: "translate(-50%, -50%)", | |||
width: "90%", | |||
maxHeight: "90%", | |||
padding: 3, | |||
display: "flex", | |||
flexDirection: "column", | |||
gap: 2, | |||
}; | |||
const TimesheetEditModal: React.FC<Props> = ({ | |||
onSave, | |||
onDelete, | |||
open, | |||
onClose, | |||
defaultValues, | |||
allProjects, | |||
assignedProjects, | |||
}) => { | |||
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]); | |||
const { | |||
register, | |||
control, | |||
reset, | |||
getValues, | |||
setValue, | |||
trigger, | |||
formState, | |||
watch, | |||
} = useForm<TimeEntry>(); | |||
useEffect(() => { | |||
reset(defaultValues ?? { id: Date.now() }); | |||
}, [defaultValues, reset]); | |||
const saveHandler = useCallback(async () => { | |||
const valid = await trigger(); | |||
if (valid) { | |||
onSave(getValues()); | |||
reset(); | |||
} | |||
}, [getValues, onSave, reset, trigger]); | |||
const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
(...args) => { | |||
onClose?.(...args); | |||
reset(); | |||
}, | |||
[onClose, reset], | |||
); | |||
const projectId = watch("projectId"); | |||
const taskGroupId = watch("taskGroupId"); | |||
const otHours = watch("otHours"); | |||
return ( | |||
<Modal open={open} onClose={closeHandler}> | |||
<Paper sx={modalSx}> | |||
<FormControl fullWidth> | |||
<InputLabel shrink>{t("Project Code and Name")}</InputLabel> | |||
<Controller | |||
control={control} | |||
name="projectId" | |||
render={({ field }) => ( | |||
<ProjectSelect | |||
allProjects={allProjects} | |||
assignedProjects={assignedProjects} | |||
value={field.value} | |||
onProjectSelect={(newId) => { | |||
field.onChange(newId ?? null); | |||
const firstTaskGroup = ( | |||
typeof newId === "number" ? taskGroupsByProject[newId] : [] | |||
)[0]; | |||
setValue("taskGroupId", firstTaskGroup?.value); | |||
setValue("taskId", undefined); | |||
}} | |||
/> | |||
)} | |||
rules={{ deps: ["taskGroupId", "taskId"] }} | |||
/> | |||
</FormControl> | |||
<FormControl fullWidth> | |||
<InputLabel shrink>{t("Stage")}</InputLabel> | |||
<Controller | |||
control={control} | |||
name="taskGroupId" | |||
render={({ field }) => ( | |||
<TaskGroupSelect | |||
error={Boolean(formState.errors.taskGroupId)} | |||
projectId={projectId} | |||
taskGroupsByProject={taskGroupsByProject} | |||
value={field.value} | |||
onTaskGroupSelect={(newId) => { | |||
field.onChange(newId ?? null); | |||
}} | |||
/> | |||
)} | |||
rules={{ | |||
validate: (id) => { | |||
if (!projectId) { | |||
return !id; | |||
} | |||
const taskGroups = taskGroupsByProject[projectId]; | |||
return taskGroups.some((tg) => tg.value === id); | |||
}, | |||
deps: ["taskId"], | |||
}} | |||
/> | |||
</FormControl> | |||
<FormControl fullWidth> | |||
<InputLabel shrink>{t("Task")}</InputLabel> | |||
<Controller | |||
control={control} | |||
name="taskId" | |||
render={({ field }) => ( | |||
<TaskSelect | |||
error={Boolean(formState.errors.taskId)} | |||
projectId={projectId} | |||
taskGroupId={taskGroupId} | |||
allProjects={allProjects} | |||
value={field.value} | |||
onTaskSelect={(newId) => { | |||
field.onChange(newId ?? null); | |||
}} | |||
/> | |||
)} | |||
rules={{ | |||
validate: (id) => { | |||
if (!projectId) { | |||
return !id; | |||
} | |||
const projectTasks = allProjects.find((p) => p.id === projectId) | |||
?.tasks; | |||
return Boolean(projectTasks?.some((task) => task.id === id)); | |||
}, | |||
}} | |||
/> | |||
</FormControl> | |||
<TextField | |||
type="number" | |||
label={t("Hours")} | |||
fullWidth | |||
{...register("inputHours", { | |||
valueAsNumber: true, | |||
validate: (value) => Boolean(value || otHours), | |||
})} | |||
error={Boolean(formState.errors.inputHours)} | |||
/> | |||
<TextField | |||
type="number" | |||
label={t("Other Hours")} | |||
fullWidth | |||
{...register("otHours", { | |||
valueAsNumber: true, | |||
})} | |||
error={Boolean(formState.errors.otHours)} | |||
/> | |||
<TextField | |||
label={t("Remark")} | |||
fullWidth | |||
multiline | |||
rows={2} | |||
error={Boolean(formState.errors.remark)} | |||
{...register("remark", { | |||
validate: (value) => Boolean(projectId || value), | |||
})} | |||
/> | |||
<Box display="flex" justifyContent="flex-end" gap={1}> | |||
{onDelete && ( | |||
<Button | |||
variant="outlined" | |||
startIcon={<Delete />} | |||
color="error" | |||
onClick={onDelete} | |||
> | |||
{t("Delete")} | |||
</Button> | |||
)} | |||
<Button | |||
variant="contained" | |||
startIcon={<Check />} | |||
onClick={saveHandler} | |||
> | |||
{t("Save")} | |||
</Button> | |||
</Box> | |||
</Paper> | |||
</Modal> | |||
); | |||
}; | |||
export default TimesheetEditModal; |