Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

248 linhas
6.7 KiB

  1. import { TimeEntry } from "@/app/api/timesheets/actions";
  2. import { Check, Delete } from "@mui/icons-material";
  3. import {
  4. Box,
  5. Button,
  6. FormControl,
  7. InputLabel,
  8. Modal,
  9. ModalProps,
  10. Paper,
  11. SxProps,
  12. TextField,
  13. } from "@mui/material";
  14. import React, { useCallback, useEffect, useMemo } from "react";
  15. import { Controller, useForm } from "react-hook-form";
  16. import { useTranslation } from "react-i18next";
  17. import ProjectSelect from "./ProjectSelect";
  18. import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
  19. import TaskGroupSelect from "./TaskGroupSelect";
  20. import TaskSelect from "./TaskSelect";
  21. import { TaskGroup } from "@/app/api/tasks";
  22. import uniqBy from "lodash/uniqBy";
  23. export interface Props extends Omit<ModalProps, "children"> {
  24. onSave: (leaveEntry: TimeEntry) => void;
  25. onDelete?: () => void;
  26. defaultValues?: Partial<TimeEntry>;
  27. allProjects: ProjectWithTasks[];
  28. assignedProjects: AssignedProject[];
  29. }
  30. const modalSx: SxProps = {
  31. position: "absolute",
  32. top: "50%",
  33. left: "50%",
  34. transform: "translate(-50%, -50%)",
  35. width: "90%",
  36. maxHeight: "90%",
  37. padding: 3,
  38. display: "flex",
  39. flexDirection: "column",
  40. gap: 2,
  41. };
  42. const TimesheetEditModal: React.FC<Props> = ({
  43. onSave,
  44. onDelete,
  45. open,
  46. onClose,
  47. defaultValues,
  48. allProjects,
  49. assignedProjects,
  50. }) => {
  51. const { t } = useTranslation("home");
  52. const taskGroupsByProject = useMemo(() => {
  53. return allProjects.reduce<{
  54. [projectId: AssignedProject["id"]]: {
  55. value: TaskGroup["id"];
  56. label: string;
  57. }[];
  58. }>((acc, project) => {
  59. return {
  60. ...acc,
  61. [project.id]: uniqBy(
  62. project.tasks.map((t) => ({
  63. value: t.taskGroup.id,
  64. label: t.taskGroup.name,
  65. })),
  66. "value",
  67. ),
  68. };
  69. }, {});
  70. }, [allProjects]);
  71. const {
  72. register,
  73. control,
  74. reset,
  75. getValues,
  76. setValue,
  77. trigger,
  78. formState,
  79. watch,
  80. } = useForm<TimeEntry>();
  81. useEffect(() => {
  82. reset(defaultValues ?? { id: Date.now() });
  83. }, [defaultValues, reset]);
  84. const saveHandler = useCallback(async () => {
  85. const valid = await trigger();
  86. if (valid) {
  87. onSave(getValues());
  88. reset();
  89. }
  90. }, [getValues, onSave, reset, trigger]);
  91. const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
  92. (...args) => {
  93. onClose?.(...args);
  94. reset();
  95. },
  96. [onClose, reset],
  97. );
  98. const projectId = watch("projectId");
  99. const taskGroupId = watch("taskGroupId");
  100. const otHours = watch("otHours");
  101. return (
  102. <Modal open={open} onClose={closeHandler}>
  103. <Paper sx={modalSx}>
  104. <FormControl fullWidth>
  105. <InputLabel shrink>{t("Project Code and Name")}</InputLabel>
  106. <Controller
  107. control={control}
  108. name="projectId"
  109. render={({ field }) => (
  110. <ProjectSelect
  111. allProjects={allProjects}
  112. assignedProjects={assignedProjects}
  113. value={field.value}
  114. onProjectSelect={(newId) => {
  115. field.onChange(newId ?? null);
  116. const firstTaskGroup = (
  117. typeof newId === "number" ? taskGroupsByProject[newId] : []
  118. )[0];
  119. setValue("taskGroupId", firstTaskGroup?.value);
  120. setValue("taskId", undefined);
  121. }}
  122. />
  123. )}
  124. rules={{ deps: ["taskGroupId", "taskId"] }}
  125. />
  126. </FormControl>
  127. <FormControl fullWidth>
  128. <InputLabel shrink>{t("Stage")}</InputLabel>
  129. <Controller
  130. control={control}
  131. name="taskGroupId"
  132. render={({ field }) => (
  133. <TaskGroupSelect
  134. error={Boolean(formState.errors.taskGroupId)}
  135. projectId={projectId}
  136. taskGroupsByProject={taskGroupsByProject}
  137. value={field.value}
  138. onTaskGroupSelect={(newId) => {
  139. field.onChange(newId ?? null);
  140. }}
  141. />
  142. )}
  143. rules={{
  144. validate: (id) => {
  145. if (!projectId) {
  146. return !id;
  147. }
  148. const taskGroups = taskGroupsByProject[projectId];
  149. return taskGroups.some((tg) => tg.value === id);
  150. },
  151. deps: ["taskId"],
  152. }}
  153. />
  154. </FormControl>
  155. <FormControl fullWidth>
  156. <InputLabel shrink>{t("Task")}</InputLabel>
  157. <Controller
  158. control={control}
  159. name="taskId"
  160. render={({ field }) => (
  161. <TaskSelect
  162. error={Boolean(formState.errors.taskId)}
  163. projectId={projectId}
  164. taskGroupId={taskGroupId}
  165. allProjects={allProjects}
  166. value={field.value}
  167. onTaskSelect={(newId) => {
  168. field.onChange(newId ?? null);
  169. }}
  170. />
  171. )}
  172. rules={{
  173. validate: (id) => {
  174. if (!projectId) {
  175. return !id;
  176. }
  177. const projectTasks = allProjects.find((p) => p.id === projectId)
  178. ?.tasks;
  179. return Boolean(projectTasks?.some((task) => task.id === id));
  180. },
  181. }}
  182. />
  183. </FormControl>
  184. <TextField
  185. type="number"
  186. label={t("Hours")}
  187. fullWidth
  188. {...register("inputHours", {
  189. valueAsNumber: true,
  190. validate: (value) => Boolean(value || otHours),
  191. })}
  192. error={Boolean(formState.errors.inputHours)}
  193. />
  194. <TextField
  195. type="number"
  196. label={t("Other Hours")}
  197. fullWidth
  198. {...register("otHours", {
  199. valueAsNumber: true,
  200. })}
  201. error={Boolean(formState.errors.otHours)}
  202. />
  203. <TextField
  204. label={t("Remark")}
  205. fullWidth
  206. multiline
  207. rows={2}
  208. error={Boolean(formState.errors.remark)}
  209. {...register("remark", {
  210. validate: (value) => Boolean(projectId || value),
  211. })}
  212. />
  213. <Box display="flex" justifyContent="flex-end" gap={1}>
  214. {onDelete && (
  215. <Button
  216. variant="outlined"
  217. startIcon={<Delete />}
  218. color="error"
  219. onClick={onDelete}
  220. >
  221. {t("Delete")}
  222. </Button>
  223. )}
  224. <Button
  225. variant="contained"
  226. startIcon={<Check />}
  227. onClick={saveHandler}
  228. >
  229. {t("Save")}
  230. </Button>
  231. </Box>
  232. </Paper>
  233. </Modal>
  234. );
  235. };
  236. export default TimesheetEditModal;