You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

172 lines
4.7 KiB

  1. import { TimeEntry, RecordTimesheetInput } from "@/app/api/timesheets/actions";
  2. import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil";
  3. import { Add, Edit } from "@mui/icons-material";
  4. import {
  5. Box,
  6. Button,
  7. Card,
  8. CardContent,
  9. IconButton,
  10. Typography,
  11. } from "@mui/material";
  12. import dayjs from "dayjs";
  13. import React, { useCallback, useMemo, useState } from "react";
  14. import { useFormContext } from "react-hook-form";
  15. import { useTranslation } from "react-i18next";
  16. import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
  17. import TimesheetEditModal, {
  18. Props as TimesheetEditModalProps,
  19. } from "./TimesheetEditModal";
  20. import TimeEntryCard from "./TimeEntryCard";
  21. import { HolidaysResult } from "@/app/api/holidays";
  22. import { getHolidayForDate } from "@/app/utils/holidayUtils";
  23. interface Props {
  24. date: string;
  25. allProjects: ProjectWithTasks[];
  26. assignedProjects: AssignedProject[];
  27. companyHolidays: HolidaysResult[];
  28. }
  29. const MobileTimesheetEntry: React.FC<Props> = ({
  30. date,
  31. allProjects,
  32. assignedProjects,
  33. companyHolidays,
  34. }) => {
  35. const {
  36. t,
  37. i18n: { language },
  38. } = useTranslation("home");
  39. const projectMap = useMemo(() => {
  40. return allProjects.reduce<{
  41. [id: ProjectWithTasks["id"]]: ProjectWithTasks;
  42. }>((acc, project) => {
  43. return { ...acc, [project.id]: project };
  44. }, {});
  45. }, [allProjects]);
  46. const dayJsObj = dayjs(date);
  47. const holiday = getHolidayForDate(date, companyHolidays);
  48. const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;
  49. const { watch, setValue, clearErrors } = useFormContext<RecordTimesheetInput>();
  50. const currentEntries = watch(date);
  51. // Edit modal
  52. const [editModalProps, setEditModalProps] = useState<
  53. Partial<TimesheetEditModalProps>
  54. >({});
  55. const [editModalOpen, setEditModalOpen] = useState(false);
  56. const openEditModal = useCallback(
  57. (defaultValues?: TimeEntry) => () => {
  58. setEditModalProps({
  59. defaultValues,
  60. onDelete: defaultValues
  61. ? () => {
  62. setValue(
  63. date,
  64. currentEntries.filter((entry) => entry.id !== defaultValues.id),
  65. );
  66. clearErrors(date);
  67. setEditModalOpen(false);
  68. }
  69. : undefined,
  70. });
  71. setEditModalOpen(true);
  72. },
  73. [clearErrors, currentEntries, date, setValue],
  74. );
  75. const closeEditModal = useCallback(() => {
  76. setEditModalOpen(false);
  77. }, []);
  78. const onSaveEntry = useCallback(
  79. async (entry: TimeEntry) => {
  80. const existingEntry = currentEntries.find((e) => e.id === entry.id);
  81. if (existingEntry) {
  82. setValue(
  83. date,
  84. currentEntries.map((e) => ({
  85. ...(e.id === existingEntry.id ? entry : e),
  86. })),
  87. );
  88. clearErrors(date);
  89. } else {
  90. setValue(date, [...currentEntries, entry]);
  91. }
  92. setEditModalOpen(false);
  93. },
  94. [clearErrors, currentEntries, date, setValue],
  95. );
  96. return (
  97. <>
  98. <Typography
  99. paddingInline={2}
  100. variant="overline"
  101. color={isHoliday ? "error.main" : undefined}
  102. >
  103. {shortDateFormatter(language).format(dayJsObj.toDate())}
  104. {holiday && (
  105. <Typography
  106. marginInlineStart={1}
  107. variant="caption"
  108. >{`(${holiday.title})`}</Typography>
  109. )}
  110. </Typography>
  111. <Box
  112. paddingInline={2}
  113. flex={1}
  114. display="flex"
  115. flexDirection="column"
  116. gap={2}
  117. overflow="scroll"
  118. >
  119. {currentEntries.length ? (
  120. currentEntries.map((entry, index) => {
  121. const project = entry.projectId
  122. ? projectMap[entry.projectId]
  123. : undefined;
  124. const task = project?.tasks.find((t) => t.id === entry.taskId);
  125. return (
  126. <TimeEntryCard
  127. key={`${entry.id}-${index}`}
  128. project={project}
  129. task={task}
  130. entry={entry}
  131. onEdit={openEditModal(entry)}
  132. />
  133. );
  134. })
  135. ) : (
  136. <Typography variant="body2" display="block">
  137. {t("Add some time entries!")}
  138. </Typography>
  139. )}
  140. <Box>
  141. <Button startIcon={<Add />} onClick={openEditModal()}>
  142. {t("Record time")}
  143. </Button>
  144. </Box>
  145. <TimesheetEditModal
  146. allProjects={allProjects}
  147. assignedProjects={assignedProjects}
  148. open={editModalOpen}
  149. onClose={closeEditModal}
  150. onSave={onSaveEntry}
  151. isHoliday={Boolean(isHoliday)}
  152. {...editModalProps}
  153. />
  154. </Box>
  155. </>
  156. );
  157. };
  158. export default MobileTimesheetEntry;