25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 

221 satır
9.0 KiB

  1. "use client";
  2. import Card from "@mui/material/Card";
  3. import CardContent from "@mui/material/CardContent";
  4. import Grid from "@mui/material/Grid";
  5. import Typography from "@mui/material/Typography";
  6. import { useTranslation } from "react-i18next";
  7. import TransferList from "../TransferList";
  8. import Button from "@mui/material/Button";
  9. import React, { SyntheticEvent, useCallback, useMemo, useState } from "react";
  10. import CardActions from "@mui/material/CardActions";
  11. import RestartAlt from "@mui/icons-material/RestartAlt";
  12. import FormControl from "@mui/material/FormControl";
  13. import Select, { SelectChangeEvent } from "@mui/material/Select";
  14. import MenuItem from "@mui/material/MenuItem";
  15. import InputLabel from "@mui/material/InputLabel";
  16. import { Task, TaskTemplate } from "@/app/api/tasks";
  17. import { useFormContext } from "react-hook-form";
  18. import { CreateProjectInputs, ManhourAllocation } from "@/app/api/projects/actions";
  19. import isNumber from "lodash/isNumber";
  20. import intersectionWith from "lodash/intersectionWith";
  21. import { difference } from "lodash";
  22. import { Autocomplete, TextField } from "@mui/material";
  23. interface Props {
  24. allTasks: Task[];
  25. taskTemplates: TaskTemplate[];
  26. isActive: boolean;
  27. }
  28. const TaskSetup: React.FC<Props> = ({
  29. allTasks: tasks,
  30. taskTemplates,
  31. isActive,
  32. }) => {
  33. const { t } = useTranslation();
  34. const { setValue, watch, clearErrors, setError, formState: { defaultValues } } = useFormContext<CreateProjectInputs>();
  35. const currentTaskGroups = watch("taskGroups");
  36. const currentTaskIds = Object.values(currentTaskGroups).reduce<Task["id"][]>(
  37. (acc, group) => {
  38. return group.taskIds && group.taskIds.length > 0 ? [...acc, ...group.taskIds] : acc;
  39. },
  40. [],
  41. );
  42. const onReset = useCallback(() => {
  43. setValue("taskGroups", {});
  44. }, [setValue]);
  45. const [selectedTaskTemplateId, setSelectedTaskTemplateId] = useState<
  46. "All" | number
  47. >(watch("taskTemplateId") ?? "All");
  48. const onSelectTaskTemplate = useCallback(
  49. (event: SyntheticEvent<Element, Event>, value: NonNullable<{id: number | string, name: string}>) => {
  50. if (value.id === "All" || isNumber(value.id)) {
  51. setSelectedTaskTemplateId(value.id);
  52. // onReset();
  53. }
  54. },
  55. [onReset],
  56. );
  57. const items = useMemo(() => {
  58. const selectedTaskTemplate = taskTemplates.find(
  59. (template) => template.id === selectedTaskTemplateId,
  60. )
  61. if (selectedTaskTemplateId !== "All" && selectedTaskTemplateId !== watch("taskTemplateId")) {
  62. // update the "manhour allocation by grade" by task template
  63. const updatedManhourPercentageByGrade: ManhourAllocation = watch("manhourPercentageByGrade")
  64. selectedTaskTemplate?.gradeAllocations.forEach((gradeAllocation) => {
  65. updatedManhourPercentageByGrade[gradeAllocation.grade.id] = gradeAllocation?.percentage
  66. })
  67. setValue("manhourPercentageByGrade", updatedManhourPercentageByGrade)
  68. if (Object.values(updatedManhourPercentageByGrade).reduce((acc, value) => acc + value, 0) === 100) clearErrors("manhourPercentageByGrade")
  69. else setError("manhourPercentageByGrade", { message: "manhourPercentageByGrade value is not valid", type: "invalid" })
  70. // update the "manhour allocation by grade by stage" by task template
  71. const updatedTaskGroups = watch("taskGroups")
  72. const taskGroupsKeys = Object.keys(updatedTaskGroups)
  73. selectedTaskTemplate?.groupAllocations.forEach((groupAllocation) => {
  74. const taskGroupId = groupAllocation.taskGroup.id
  75. if (taskGroupsKeys.includes(taskGroupId.toString())) {
  76. updatedTaskGroups[taskGroupId] = { ...updatedTaskGroups[taskGroupId], percentAllocation: groupAllocation?.percentage }
  77. }
  78. })
  79. const percentageToZeroGroupIds = difference(taskGroupsKeys.map(key => parseFloat(key)), selectedTaskTemplate?.groupAllocations.map(groupAllocation => groupAllocation.taskGroup.id)!!)
  80. percentageToZeroGroupIds.forEach((percentageToZeroGroupId) => {
  81. updatedTaskGroups[percentageToZeroGroupId] = { ...updatedTaskGroups[percentageToZeroGroupId], percentAllocation: 0 }
  82. })
  83. setValue("taskGroups", updatedTaskGroups)
  84. if (Object.values(updatedTaskGroups).reduce((acc, value) => acc + value.percentAllocation, 0) === 100) clearErrors("taskGroups")
  85. else setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" })
  86. }
  87. setValue("taskTemplateId", selectedTaskTemplateId)
  88. const taskList =
  89. selectedTaskTemplateId === "All"
  90. ? tasks
  91. : selectedTaskTemplate?.tasks || tasks;
  92. return taskList.map((t) => ({
  93. id: t.id,
  94. label: t.name,
  95. group: t.taskGroup,
  96. }));
  97. }, [tasks, selectedTaskTemplateId, taskTemplates]);
  98. const selectedItems = useMemo(() => {
  99. return intersectionWith(
  100. tasks,
  101. currentTaskIds,
  102. (task, taskId) => task.id === taskId,
  103. ).map((t) => ({ id: t.id, label: t.name, group: t.taskGroup }));
  104. }, [currentTaskIds, tasks]);
  105. return (
  106. <Card sx={{ display: isActive ? "block" : "none" }}>
  107. <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
  108. <Typography variant="overline" display="block" marginBlockEnd={1}>
  109. {t("Task List Setup")}
  110. </Typography>
  111. <Grid
  112. container
  113. spacing={2}
  114. columns={{ xs: 6, sm: 12 }}
  115. marginBlockEnd={1}
  116. >
  117. <Grid item xs={6}>
  118. <Autocomplete
  119. disableClearable
  120. // disablePortal
  121. noOptionsText={t("No Task List Source")}
  122. value={taskTemplates.find(taskTemplate => taskTemplate.id === selectedTaskTemplateId)}
  123. options={[{id: "All", name: t("All tasks")}, ...taskTemplates.map(taskTemplate => ({id: taskTemplate.id, name: taskTemplate.name}))]}
  124. getOptionLabel={(taskTemplate) => taskTemplate.name}
  125. isOptionEqualToValue={(option, value) => option?.id === value?.id}
  126. renderOption={(params, option) => {
  127. return (
  128. <MenuItem {...params} key={option.id} value={option.id}>
  129. {option.name}
  130. </MenuItem>
  131. );
  132. }}
  133. onChange={onSelectTaskTemplate}
  134. renderInput={(params) => <TextField {...params} variant="outlined" label={t("Task List Source")} />}
  135. />
  136. </Grid>
  137. </Grid>
  138. <TransferList
  139. allItems={items}
  140. selectedItems={selectedItems}
  141. onChange={(selectedTasks) => {
  142. const newTaskGroups = selectedTasks.reduce<
  143. CreateProjectInputs["taskGroups"]
  144. >((acc, item) => {
  145. if (!item.group) {
  146. // TODO: this should not happen (all tasks are part of a group)
  147. return acc;
  148. }
  149. if (!acc[item.group.id]) {
  150. return {
  151. ...acc,
  152. [item.group.id]: {
  153. taskIds: [item.id],
  154. percentAllocation:
  155. currentTaskGroups[item.group.id]?.percentAllocation || 0,
  156. },
  157. };
  158. }
  159. return {
  160. ...acc,
  161. [item.group.id]: {
  162. ...acc[item.group.id],
  163. taskIds: [...acc[item.group.id].taskIds, item.id],
  164. },
  165. };
  166. }, {});
  167. // update the "manhour allocation by grade by stage" by task template
  168. const taskGroupsKeys = Object.keys(newTaskGroups)
  169. const selectedTaskTemplate = taskTemplates.find(
  170. (template) => template.id === selectedTaskTemplateId,
  171. )
  172. selectedTaskTemplate?.groupAllocations.forEach((groupAllocation) => {
  173. const taskGroupId = groupAllocation.taskGroup.id
  174. if (taskGroupsKeys.includes(taskGroupId.toString())) {
  175. newTaskGroups[taskGroupId] = { ...newTaskGroups[taskGroupId], percentAllocation: groupAllocation?.percentage }
  176. }
  177. })
  178. const percentageToZeroGroupIds = difference(taskGroupsKeys.map(key => parseFloat(key)), selectedTaskTemplate?.groupAllocations.map(groupAllocation => groupAllocation.taskGroup.id)!!)
  179. percentageToZeroGroupIds.forEach((percentageToZeroGroupId) => {
  180. newTaskGroups[percentageToZeroGroupId] = { ...newTaskGroups[percentageToZeroGroupId], percentAllocation: 0 }
  181. })
  182. setValue("taskGroups", newTaskGroups)
  183. if (Object.values(newTaskGroups).reduce((acc, value) => acc + value.percentAllocation, 0) === 100) clearErrors("taskGroups")
  184. else setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" })
  185. }}
  186. allItemsLabel={t("Task Pool")}
  187. selectedItemsLabel={t("Project Task List")}
  188. />
  189. {/* <CardActions sx={{ justifyContent: "flex-end" }}>
  190. <Button variant="text" startIcon={<RestartAlt />} onClick={onReset}>
  191. {t("Reset")}
  192. </Button>
  193. </CardActions> */}
  194. </CardContent>
  195. </Card>
  196. );
  197. };
  198. export default TaskSetup;