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.
 
 

226 lignes
6.3 KiB

  1. import React, { useCallback, useEffect, useMemo } from "react";
  2. import {
  3. Box,
  4. Button,
  5. Card,
  6. CardActions,
  7. CardContent,
  8. Modal,
  9. ModalProps,
  10. SxProps,
  11. Typography,
  12. } from "@mui/material";
  13. import TimesheetTable from "../TimesheetTable";
  14. import { useTranslation } from "react-i18next";
  15. import { Check, Close } from "@mui/icons-material";
  16. import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
  17. import {
  18. RecordLeaveInput,
  19. RecordTimesheetInput,
  20. saveTimesheet,
  21. } from "@/app/api/timesheets/actions";
  22. import dayjs from "dayjs";
  23. import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  24. import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
  25. import FullscreenModal from "../FullscreenModal";
  26. import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable";
  27. import useIsMobile from "@/app/utils/useIsMobile";
  28. import { HolidaysResult } from "@/app/api/holidays";
  29. import {
  30. DAILY_NORMAL_MAX_HOURS,
  31. TIMESHEET_DAILY_MAX_HOURS,
  32. validateTimesheet,
  33. } from "@/app/api/timesheets/utils";
  34. import ErrorAlert from "../ErrorAlert";
  35. interface Props {
  36. isOpen: boolean;
  37. onClose: () => void;
  38. allProjects: ProjectWithTasks[];
  39. assignedProjects: AssignedProject[];
  40. username: string;
  41. defaultTimesheets?: RecordTimesheetInput;
  42. leaveRecords: RecordLeaveInput;
  43. companyHolidays: HolidaysResult[];
  44. fastEntryEnabled?: boolean;
  45. }
  46. const modalSx: SxProps = {
  47. position: "absolute",
  48. top: "50%",
  49. left: "50%",
  50. transform: "translate(-50%, -50%)",
  51. width: { xs: "calc(100% - 2rem)", sm: "90%" },
  52. maxHeight: "90%",
  53. maxWidth: 1400,
  54. };
  55. const TimesheetModal: React.FC<Props> = ({
  56. isOpen,
  57. onClose,
  58. allProjects,
  59. assignedProjects,
  60. username,
  61. defaultTimesheets,
  62. leaveRecords,
  63. companyHolidays,
  64. fastEntryEnabled,
  65. }) => {
  66. const { t } = useTranslation("home");
  67. const defaultValues = useMemo(() => {
  68. const today = dayjs();
  69. return Array(7)
  70. .fill(undefined)
  71. .reduce<RecordTimesheetInput>((acc, _, index) => {
  72. const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT);
  73. return {
  74. ...acc,
  75. [date]: defaultTimesheets?.[date] ?? [],
  76. };
  77. }, {});
  78. }, [defaultTimesheets]);
  79. const formProps = useForm<RecordTimesheetInput>({ defaultValues });
  80. useEffect(() => {
  81. formProps.reset(defaultValues);
  82. }, [defaultValues, formProps]);
  83. const onSubmit = useCallback<SubmitHandler<RecordTimesheetInput>>(
  84. async (data) => {
  85. const errors = validateTimesheet(data, leaveRecords, companyHolidays);
  86. if (errors) {
  87. Object.keys(errors).forEach((date) =>
  88. formProps.setError(date, {
  89. message: errors[date],
  90. }),
  91. );
  92. return;
  93. }
  94. const savedRecords = await saveTimesheet(data, username);
  95. const today = dayjs();
  96. const newFormValues = Array(7)
  97. .fill(undefined)
  98. .reduce<RecordTimesheetInput>((acc, _, index) => {
  99. const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT);
  100. return {
  101. ...acc,
  102. [date]: savedRecords[date] ?? [],
  103. };
  104. }, {});
  105. formProps.reset(newFormValues);
  106. onClose();
  107. },
  108. [companyHolidays, formProps, leaveRecords, onClose, username],
  109. );
  110. const onCancel = useCallback(() => {
  111. formProps.reset(defaultValues);
  112. onClose();
  113. }, [defaultValues, formProps, onClose]);
  114. const onModalClose = useCallback<NonNullable<ModalProps["onClose"]>>(
  115. (_, reason) => {
  116. if (reason !== "backdropClick") {
  117. onClose();
  118. }
  119. },
  120. [onClose],
  121. );
  122. const errorComponent = (
  123. <ErrorAlert
  124. errors={Object.keys(formProps.formState.errors).map((date) => {
  125. const error = formProps.formState.errors[date]?.message;
  126. return error
  127. ? `${date}: ${t(error, {
  128. TIMESHEET_DAILY_MAX_HOURS,
  129. DAILY_NORMAL_MAX_HOURS,
  130. })}`
  131. : undefined;
  132. })}
  133. />
  134. );
  135. const matches = useIsMobile();
  136. return (
  137. <FormProvider {...formProps}>
  138. {!matches ? (
  139. // Desktop version
  140. <Modal open={isOpen} onClose={onModalClose}>
  141. <Card sx={modalSx}>
  142. <CardContent
  143. component="form"
  144. onSubmit={formProps.handleSubmit(onSubmit)}
  145. >
  146. <Typography variant="overline" display="block" marginBlockEnd={1}>
  147. {t("Timesheet Input")}
  148. </Typography>
  149. <Box
  150. sx={{
  151. marginInline: -3,
  152. marginBlock: 4,
  153. }}
  154. >
  155. <TimesheetTable
  156. companyHolidays={companyHolidays}
  157. assignedProjects={assignedProjects}
  158. allProjects={allProjects}
  159. leaveRecords={leaveRecords}
  160. fastEntryEnabled={fastEntryEnabled}
  161. />
  162. </Box>
  163. {errorComponent}
  164. <CardActions sx={{ justifyContent: "flex-end" }}>
  165. <Button
  166. variant="outlined"
  167. startIcon={<Close />}
  168. onClick={onCancel}
  169. >
  170. {t("Cancel")}
  171. </Button>
  172. <Button variant="contained" startIcon={<Check />} type="submit">
  173. {t("Save")}
  174. </Button>
  175. </CardActions>
  176. </CardContent>
  177. </Card>
  178. </Modal>
  179. ) : (
  180. // Mobile version
  181. <FullscreenModal
  182. open={isOpen}
  183. onClose={onModalClose}
  184. closeModal={onCancel}
  185. >
  186. <Box
  187. display="flex"
  188. flexDirection="column"
  189. gap={2}
  190. height="100%"
  191. component="form"
  192. onSubmit={formProps.handleSubmit(onSubmit)}
  193. >
  194. <Typography variant="h6" padding={2} flex="none">
  195. {t("Timesheet Input")}
  196. </Typography>
  197. <MobileTimesheetTable
  198. fastEntryEnabled={fastEntryEnabled}
  199. companyHolidays={companyHolidays}
  200. assignedProjects={assignedProjects}
  201. allProjects={allProjects}
  202. leaveRecords={leaveRecords}
  203. errorComponent={errorComponent}
  204. />
  205. </Box>
  206. </FullscreenModal>
  207. )}
  208. </FormProvider>
  209. );
  210. };
  211. export default TimesheetModal;