Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

787 lignes
23 KiB

  1. import { Add, Check, Close, Delete } from "@mui/icons-material";
  2. import { Box, Button, Tooltip, Typography } from "@mui/material";
  3. import {
  4. FooterPropsOverrides,
  5. GridActionsCellItem,
  6. GridCellParams,
  7. GridColDef,
  8. GridEditInputCell,
  9. GridRenderCellParams,
  10. GridRenderEditCellParams,
  11. GridRowId,
  12. GridRowIdGetter,
  13. GridRowModel,
  14. GridRowModes,
  15. GridRowModesModel,
  16. GridToolbarContainer,
  17. useGridApiRef,
  18. } from "@mui/x-data-grid";
  19. import { useTranslation } from "react-i18next";
  20. import StyledDataGrid from "../StyledDataGrid";
  21. import React, { useCallback, useEffect, useMemo, useState } from "react";
  22. import { useFormContext } from "react-hook-form";
  23. import {
  24. RecordTimeLeaveInput,
  25. TimeEntry,
  26. TimeLeaveEntry,
  27. } from "@/app/api/timesheets/actions";
  28. import { manhourFormatter } from "@/app/utils/formatUtil";
  29. import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
  30. import uniqBy from "lodash/uniqBy";
  31. import { Task, TaskGroup } from "@/app/api/tasks";
  32. import dayjs from "dayjs";
  33. import isBetween from "dayjs/plugin/isBetween";
  34. import ProjectSelect from "../TimesheetTable/ProjectSelect";
  35. import TaskGroupSelect, {
  36. TaskGroupSelectWithoutProject,
  37. } from "../TimesheetTable/TaskGroupSelect";
  38. import TaskSelect from "../TimesheetTable/TaskSelect";
  39. import {
  40. DAILY_NORMAL_MAX_HOURS,
  41. LeaveEntryError,
  42. TimeEntryError,
  43. validateLeaveEntry,
  44. validateTimeEntry,
  45. } from "@/app/api/timesheets/utils";
  46. import { roundToNearestQuarter } from "@/app/utils/manhourUtils";
  47. import FastTimeEntryModal from "../TimesheetTable/FastTimeEntryModal";
  48. import { LeaveType } from "@/app/api/timesheets";
  49. import DisabledEdit from "./DisabledEdit";
  50. import TwoLineCell from "./TwoLineCell";
  51. dayjs.extend(isBetween);
  52. interface Props {
  53. day: string;
  54. isHoliday: boolean;
  55. allProjects: ProjectWithTasks[];
  56. assignedProjects: AssignedProject[];
  57. fastEntryEnabled?: boolean;
  58. leaveTypes: LeaveType[];
  59. miscTasks: Task[];
  60. }
  61. export type TimeLeaveRow = Partial<
  62. TimeLeaveEntry & {
  63. _isNew: boolean;
  64. _error: TimeEntryError | LeaveEntryError;
  65. _isPlanned?: boolean;
  66. }
  67. >;
  68. class ProcessRowUpdateError extends Error {
  69. public readonly row: TimeLeaveRow;
  70. public readonly errors: TimeEntryError | LeaveEntryError | undefined;
  71. constructor(
  72. row: TimeLeaveRow,
  73. message?: string,
  74. errors?: TimeEntryError | LeaveEntryError,
  75. ) {
  76. super(message);
  77. this.row = row;
  78. this.errors = errors;
  79. Object.setPrototypeOf(this, ProcessRowUpdateError.prototype);
  80. }
  81. }
  82. const TimeLeaveInputTable: React.FC<Props> = ({
  83. day,
  84. allProjects,
  85. assignedProjects,
  86. isHoliday,
  87. fastEntryEnabled,
  88. leaveTypes,
  89. miscTasks,
  90. }) => {
  91. const { t } = useTranslation("home");
  92. const taskGroupsByProject = useMemo(() => {
  93. return allProjects.reduce<{
  94. [projectId: AssignedProject["id"]]: {
  95. value: TaskGroup["id"];
  96. label: string;
  97. }[];
  98. }>((acc, project) => {
  99. return {
  100. ...acc,
  101. [project.id]: uniqBy(
  102. project.tasks.map((t) => ({
  103. value: t.taskGroup.id,
  104. label: t.taskGroup.name,
  105. })),
  106. "value",
  107. ),
  108. };
  109. }, {});
  110. }, [allProjects]);
  111. const taskGroupsWithoutProject = useMemo(
  112. () =>
  113. uniqBy(
  114. miscTasks.map((t) => t.taskGroup),
  115. "id",
  116. ),
  117. [miscTasks],
  118. );
  119. // To check for start / end planned dates
  120. const milestonesByProject = useMemo(() => {
  121. return assignedProjects.reduce<{
  122. [projectId: AssignedProject["id"]]: AssignedProject["milestones"];
  123. }>((acc, project) => {
  124. return { ...acc, [project.id]: { ...project.milestones } };
  125. }, {});
  126. }, [assignedProjects]);
  127. const { getValues, setValue, clearErrors, setError } =
  128. useFormContext<RecordTimeLeaveInput>();
  129. const currentEntries = getValues(day);
  130. const getRowId = useCallback<GridRowIdGetter<TimeLeaveRow>>(
  131. (row) => `${row.type}-${row.id}`,
  132. [],
  133. );
  134. const [entries, setEntries] = useState<TimeLeaveRow[]>(currentEntries || []);
  135. const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
  136. const apiRef = useGridApiRef();
  137. const addRow = useCallback(() => {
  138. const type = "timeEntry" as const;
  139. const newEntry = { id: Date.now(), _isNew: true, type };
  140. setEntries((e) => [...e, newEntry]);
  141. setRowModesModel((model) => ({
  142. ...model,
  143. [getRowId(newEntry)]: {
  144. mode: GridRowModes.Edit,
  145. fieldToFocus: "projectId",
  146. },
  147. }));
  148. }, [getRowId]);
  149. const validateRow = useCallback(
  150. (row: TimeLeaveRow) => {
  151. if (row.type === "timeEntry") {
  152. return validateTimeEntry(row, isHoliday);
  153. } else {
  154. return validateLeaveEntry(row, isHoliday);
  155. }
  156. },
  157. [isHoliday],
  158. );
  159. const verifyIsPlanned = useCallback(
  160. (row: TimeLeaveRow) => {
  161. if (
  162. row.type === "timeEntry" &&
  163. row.projectId &&
  164. row.taskGroupId &&
  165. milestonesByProject[row.projectId]
  166. ) {
  167. const milestone =
  168. milestonesByProject[row.projectId][row.taskGroupId] || {};
  169. const { startDate, endDate } = milestone;
  170. // Check if the current day is between the start and end date inclusively
  171. return dayjs(day).isBetween(startDate, endDate, "day", "[]");
  172. }
  173. },
  174. [day, milestonesByProject],
  175. );
  176. const handleCancel = useCallback(
  177. (id: GridRowId) => () => {
  178. setRowModesModel((model) => ({
  179. ...model,
  180. [id]: { mode: GridRowModes.View, ignoreModifications: true },
  181. }));
  182. const editedRow = entries.find((entry) => getRowId(entry) === id);
  183. if (editedRow?._isNew) {
  184. setEntries((es) => es.filter((e) => getRowId(e) !== id));
  185. } else {
  186. setEntries((es) =>
  187. es.map((e) =>
  188. getRowId(e) === id
  189. ? { ...e, _error: undefined, _isPlanned: undefined }
  190. : e,
  191. ),
  192. );
  193. }
  194. },
  195. [entries, getRowId],
  196. );
  197. const handleDelete = useCallback(
  198. (id: GridRowId) => () => {
  199. setEntries((es) => es.filter((e) => getRowId(e) !== id));
  200. },
  201. [getRowId],
  202. );
  203. const handleSave = useCallback(
  204. (id: GridRowId) => () => {
  205. setRowModesModel((model) => ({
  206. ...model,
  207. [id]: { mode: GridRowModes.View },
  208. }));
  209. },
  210. [],
  211. );
  212. const processRowUpdate = useCallback(
  213. (
  214. newRow: GridRowModel<TimeLeaveRow>,
  215. originalRow: GridRowModel<TimeLeaveRow>,
  216. ) => {
  217. const errors = validateRow(newRow);
  218. if (errors) {
  219. throw new ProcessRowUpdateError(
  220. originalRow,
  221. "validation error",
  222. errors,
  223. );
  224. }
  225. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  226. const { _isNew, _error, _isPlanned, ...updatedRow } = newRow;
  227. const newIsPlanned = verifyIsPlanned(updatedRow);
  228. const rowToSave = {
  229. ...updatedRow,
  230. ...(updatedRow.type === "timeEntry"
  231. ? {
  232. leaveTypeId: undefined,
  233. }
  234. : {
  235. projectId: undefined,
  236. taskGroupId: undefined,
  237. taskId: undefined,
  238. }),
  239. _isPlanned: newIsPlanned,
  240. } satisfies TimeLeaveRow;
  241. setEntries((es) =>
  242. es.map((e) => (getRowId(e) === getRowId(originalRow) ? rowToSave : e)),
  243. );
  244. return rowToSave;
  245. },
  246. [validateRow, verifyIsPlanned, getRowId],
  247. );
  248. const onProcessRowUpdateError = useCallback(
  249. (updateError: ProcessRowUpdateError) => {
  250. const errors = updateError.errors;
  251. const oldRow = updateError.row;
  252. apiRef.current.updateRows([{ ...oldRow, _error: errors }]);
  253. },
  254. [apiRef],
  255. );
  256. const columns = useMemo<GridColDef[]>(
  257. () => [
  258. {
  259. field: "projectId",
  260. editable: true,
  261. },
  262. {
  263. field: "leaveTypeId",
  264. editable: true,
  265. },
  266. {
  267. field: "type",
  268. headerName: t("Project or Leave"),
  269. width: 300,
  270. editable: true,
  271. valueFormatter(params) {
  272. const row = params.id
  273. ? params.api.getRow<TimeLeaveRow>(params.id)
  274. : null;
  275. if (!row) {
  276. return null;
  277. }
  278. if (row.type === "timeEntry") {
  279. const project = allProjects.find((p) => p.id === row.projectId);
  280. return project ? `${project.code} - ${project.name}` : t("None");
  281. } else if (row.type === "leaveEntry") {
  282. const leave = leaveTypes.find((l) => l.id === row.leaveTypeId);
  283. return leave?.name || "Unknown leave";
  284. }
  285. },
  286. renderCell(params: GridRenderCellParams<TimeLeaveRow, number>) {
  287. return <TwoLineCell>{params.formattedValue}</TwoLineCell>;
  288. },
  289. renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow, number>) {
  290. return (
  291. <ProjectSelect
  292. referenceDay={dayjs(day)}
  293. includeLeaves
  294. leaveTypes={leaveTypes}
  295. multiple={false}
  296. allProjects={allProjects}
  297. assignedProjects={assignedProjects}
  298. value={
  299. (params.row.type === "leaveEntry"
  300. ? `leave-${params.row.leaveTypeId}`
  301. : undefined) ||
  302. (params.row.type === "timeEntry"
  303. ? params.row.projectId
  304. : undefined)
  305. }
  306. onProjectSelect={async (projectOrLeaveId, isLeave) => {
  307. await params.api.setEditCellValue({
  308. id: params.id,
  309. field: params.field,
  310. value: isLeave ? "leaveEntry" : "timeEntry",
  311. });
  312. await params.api.setEditCellValue({
  313. id: params.id,
  314. field: isLeave ? "leaveTypeId" : "projectId",
  315. value: projectOrLeaveId,
  316. });
  317. await params.api.setEditCellValue({
  318. id: params.id,
  319. field: isLeave ? "projectId" : "leaveTypeId",
  320. value: undefined,
  321. });
  322. await params.api.setEditCellValue({
  323. id: params.id,
  324. field: "taskGroupId",
  325. value: undefined,
  326. });
  327. await params.api.setEditCellValue({
  328. id: params.id,
  329. field: "taskId",
  330. value: undefined,
  331. });
  332. params.api.setCellFocus(
  333. params.id,
  334. isLeave || !projectOrLeaveId ? "inputHours" : "taskGroupId",
  335. );
  336. }}
  337. />
  338. );
  339. },
  340. },
  341. {
  342. field: "taskGroupId",
  343. headerName: t("Stage"),
  344. width: 200,
  345. editable: true,
  346. renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow, number>) {
  347. if (params.row.type === "timeEntry") {
  348. if (params.row.projectId) {
  349. return (
  350. <TaskGroupSelect
  351. projectId={params.row.projectId}
  352. value={params.value}
  353. taskGroupsByProject={taskGroupsByProject}
  354. onTaskGroupSelect={async (taskGroupId) => {
  355. await params.api.setEditCellValue({
  356. id: params.id,
  357. field: params.field,
  358. value: taskGroupId,
  359. });
  360. await params.api.setEditCellValue({
  361. id: params.id,
  362. field: "taskId",
  363. value: undefined,
  364. });
  365. params.api.setCellFocus(params.id, "taskId");
  366. }}
  367. />
  368. );
  369. } else {
  370. return (
  371. <TaskGroupSelectWithoutProject
  372. value={params.value}
  373. onTaskGroupSelect={async (taskGroupId) => {
  374. await params.api.setEditCellValue({
  375. id: params.id,
  376. field: params.field,
  377. value: taskGroupId,
  378. });
  379. await params.api.setEditCellValue({
  380. id: params.id,
  381. field: "taskId",
  382. value: undefined,
  383. });
  384. params.api.setCellFocus(params.id, "taskId");
  385. }}
  386. taskGroups={taskGroupsWithoutProject}
  387. />
  388. );
  389. }
  390. } else {
  391. return <DisabledEdit />;
  392. }
  393. },
  394. valueFormatter(params) {
  395. if (!params.id) {
  396. return null;
  397. }
  398. const projectId = params.api.getRow(params.id).projectId;
  399. if (projectId) {
  400. const taskGroups =
  401. taskGroupsByProject[params.api.getRow(params.id).projectId] || [];
  402. const taskGroup = taskGroups.find(
  403. (tg) => tg.value === params.value,
  404. );
  405. return taskGroup ? taskGroup.label : t("None");
  406. } else {
  407. const taskGroupId = params.value;
  408. return (
  409. taskGroupsWithoutProject.find((tg) => tg.id === taskGroupId)
  410. ?.name || t("None")
  411. );
  412. }
  413. },
  414. renderCell(params: GridRenderCellParams<TimeLeaveRow, number>) {
  415. return <TwoLineCell>{params.formattedValue}</TwoLineCell>;
  416. },
  417. },
  418. {
  419. field: "taskId",
  420. headerName: t("Task"),
  421. width: 200,
  422. editable: true,
  423. renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow, number>) {
  424. if (params.row.type === "timeEntry") {
  425. return (
  426. <TaskSelect
  427. value={params.value}
  428. projectId={params.row.projectId}
  429. taskGroupId={params.row.taskGroupId}
  430. allProjects={allProjects}
  431. onTaskSelect={(taskId) => {
  432. params.api.setEditCellValue({
  433. id: params.id,
  434. field: params.field,
  435. value: taskId,
  436. });
  437. params.api.setCellFocus(params.id, "inputHours");
  438. }}
  439. miscTasks={miscTasks}
  440. />
  441. );
  442. } else {
  443. return <DisabledEdit />;
  444. }
  445. },
  446. valueFormatter(params) {
  447. const projectId = params.id
  448. ? params.api.getRow(params.id).projectId
  449. : undefined;
  450. const task = (
  451. projectId
  452. ? allProjects.find((p) => p.id === projectId)?.tasks || []
  453. : miscTasks
  454. ).find((t) => t.id === params.value);
  455. return task ? task.name : t("None");
  456. },
  457. renderCell(params: GridRenderCellParams<TimeLeaveRow, number>) {
  458. return <TwoLineCell>{params.formattedValue}</TwoLineCell>;
  459. },
  460. },
  461. {
  462. field: "inputHours",
  463. headerName: t("Hours"),
  464. width: 100,
  465. editable: true,
  466. type: "number",
  467. renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow>) {
  468. const errorMessage =
  469. params.row._error?.[
  470. params.field as keyof Omit<TimeLeaveEntry, "type">
  471. ];
  472. const content = (
  473. <GridEditInputCell {...params} inputProps={{ min: 0 }} />
  474. );
  475. return errorMessage ? (
  476. <Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}>
  477. <Box width="100%">{content}</Box>
  478. </Tooltip>
  479. ) : (
  480. content
  481. );
  482. },
  483. valueParser(value) {
  484. return value ? roundToNearestQuarter(value) : value;
  485. },
  486. valueFormatter(params) {
  487. return manhourFormatter.format(params.value || 0);
  488. },
  489. },
  490. {
  491. field: "otHours",
  492. headerName: t("Other Hours"),
  493. width: 150,
  494. editable: true,
  495. type: "number",
  496. renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow>) {
  497. if (params.row.type === "leaveEntry") {
  498. return <DisabledEdit />;
  499. }
  500. const errorMessage =
  501. params.row._error?.[
  502. params.field as keyof Omit<TimeLeaveEntry, "type">
  503. ];
  504. const content = (
  505. <GridEditInputCell {...params} inputProps={{ min: 0 }} />
  506. );
  507. return errorMessage ? (
  508. <Tooltip title={t(errorMessage)}>
  509. <Box width="100%">{content}</Box>
  510. </Tooltip>
  511. ) : (
  512. content
  513. );
  514. },
  515. valueParser(value) {
  516. return value ? roundToNearestQuarter(value) : value;
  517. },
  518. valueFormatter(params) {
  519. return manhourFormatter.format(params.value || 0);
  520. },
  521. },
  522. {
  523. field: "remark",
  524. headerName: t("Remarks"),
  525. sortable: false,
  526. flex: 1,
  527. editable: true,
  528. renderEditCell(params: GridRenderEditCellParams<TimeLeaveRow>) {
  529. const errorMessage =
  530. params.row._error?.[
  531. params.field as keyof Omit<TimeLeaveEntry, "type">
  532. ];
  533. const content = <GridEditInputCell {...params} />;
  534. return errorMessage ? (
  535. <Tooltip title={t(errorMessage)}>
  536. <Box width="100%">{content}</Box>
  537. </Tooltip>
  538. ) : (
  539. content
  540. );
  541. },
  542. renderCell(params: GridRenderCellParams<TimeLeaveRow>) {
  543. return <TwoLineCell>{params.value}</TwoLineCell>;
  544. },
  545. },
  546. {
  547. type: "actions",
  548. field: "actions",
  549. headerName: t("Actions"),
  550. getActions: ({ id }) => {
  551. if (rowModesModel[id]?.mode === GridRowModes.Edit) {
  552. return [
  553. <GridActionsCellItem
  554. key="accpet-action"
  555. icon={<Check />}
  556. label={t("Save")}
  557. onClick={handleSave(id)}
  558. />,
  559. <GridActionsCellItem
  560. key="cancel-action"
  561. icon={<Close />}
  562. label={t("Cancel")}
  563. onClick={handleCancel(id)}
  564. />,
  565. ];
  566. }
  567. return [
  568. <GridActionsCellItem
  569. key="delete-action"
  570. icon={<Delete />}
  571. label={t("Remove")}
  572. onClick={handleDelete(id)}
  573. />,
  574. ];
  575. },
  576. },
  577. ],
  578. [
  579. t,
  580. rowModesModel,
  581. handleDelete,
  582. handleSave,
  583. handleCancel,
  584. allProjects,
  585. leaveTypes,
  586. assignedProjects,
  587. taskGroupsByProject,
  588. taskGroupsWithoutProject,
  589. miscTasks,
  590. day,
  591. ],
  592. );
  593. useEffect(() => {
  594. const newEntries: TimeLeaveEntry[] = entries
  595. .map((e) => {
  596. if (e._isNew || e._error || !e.id || !e.type) {
  597. return null;
  598. }
  599. return e;
  600. })
  601. .filter((e): e is TimeLeaveEntry => Boolean(e));
  602. setValue(day, newEntries);
  603. if (entries.some((e) => e._isNew)) {
  604. setError(day, {
  605. message: "There are some unsaved entries.",
  606. type: "custom",
  607. });
  608. } else {
  609. clearErrors(day);
  610. }
  611. }, [getValues, entries, setValue, day, clearErrors, setError]);
  612. const hasOutOfPlannedStages = entries.some(
  613. (entry) => entry._isPlanned !== undefined && !entry._isPlanned,
  614. );
  615. // Fast entry modal
  616. const [fastEntryModalOpen, setFastEntryModalOpen] = useState(false);
  617. const closeFastEntryModal = useCallback(() => {
  618. setFastEntryModalOpen(false);
  619. }, []);
  620. const openFastEntryModal = useCallback(() => {
  621. setFastEntryModalOpen(true);
  622. }, []);
  623. const onSaveFastEntry = useCallback(async (entries: TimeEntry[]) => {
  624. setEntries((e) => [
  625. ...e,
  626. ...entries.map((newEntry) => ({
  627. ...newEntry,
  628. type: "timeEntry" as const,
  629. })),
  630. ]);
  631. setFastEntryModalOpen(false);
  632. }, []);
  633. const footer = (
  634. <Box display="flex" gap={2} alignItems="center">
  635. <Button
  636. disableRipple
  637. variant="outlined"
  638. startIcon={<Add />}
  639. onClick={addRow}
  640. size="small"
  641. >
  642. {t("Record time or leave")}
  643. </Button>
  644. {fastEntryEnabled && (
  645. <Button
  646. disableRipple
  647. variant="outlined"
  648. startIcon={<Add />}
  649. onClick={openFastEntryModal}
  650. size="small"
  651. >
  652. {t("Fast time entry")}
  653. </Button>
  654. )}
  655. {hasOutOfPlannedStages && (
  656. <Typography color="warning.main" variant="body2">
  657. {t("There are entries for stages out of planned dates!")}
  658. </Typography>
  659. )}
  660. </Box>
  661. );
  662. return (
  663. <>
  664. <StyledDataGrid
  665. getRowId={getRowId}
  666. apiRef={apiRef}
  667. autoHeight
  668. sx={{
  669. "--DataGrid-overlayHeight": "100px",
  670. ".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
  671. border: "1px solid",
  672. borderColor: "error.main",
  673. },
  674. ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
  675. border: "1px solid",
  676. borderColor: "warning.main",
  677. },
  678. }}
  679. disableColumnMenu
  680. editMode="row"
  681. columnVisibilityModel={{ projectId: false, leaveTypeId: false }}
  682. rows={entries}
  683. rowModesModel={rowModesModel}
  684. onRowModesModelChange={setRowModesModel}
  685. processRowUpdate={processRowUpdate}
  686. onProcessRowUpdateError={onProcessRowUpdateError}
  687. columns={columns}
  688. getCellClassName={(params: GridCellParams<TimeLeaveRow>) => {
  689. let classname = "";
  690. if (
  691. params.row._error?.[
  692. params.field as keyof Omit<TimeLeaveEntry, "type">
  693. ]
  694. ) {
  695. classname = "hasError";
  696. } else if (
  697. params.field === "taskGroupId" &&
  698. params.row._isPlanned !== undefined &&
  699. !params.row._isPlanned
  700. ) {
  701. classname = "hasWarning";
  702. }
  703. return classname;
  704. }}
  705. slots={{
  706. footer: FooterToolbar,
  707. noRowsOverlay: NoRowsOverlay,
  708. }}
  709. slotProps={{
  710. footer: { child: footer },
  711. }}
  712. />
  713. {fastEntryEnabled && (
  714. <FastTimeEntryModal
  715. recordDate={day}
  716. allProjects={allProjects}
  717. assignedProjects={assignedProjects}
  718. open={fastEntryModalOpen}
  719. isHoliday={Boolean(isHoliday)}
  720. onClose={closeFastEntryModal}
  721. onSave={onSaveFastEntry}
  722. />
  723. )}
  724. </>
  725. );
  726. };
  727. const NoRowsOverlay: React.FC = () => {
  728. const { t } = useTranslation("home");
  729. return (
  730. <Box
  731. display="flex"
  732. justifyContent="center"
  733. alignItems="center"
  734. height="100%"
  735. >
  736. <Typography variant="caption">{t("Add some time entries!")}</Typography>
  737. </Box>
  738. );
  739. };
  740. const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
  741. return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
  742. };
  743. export default TimeLeaveInputTable;