| @@ -1,4 +1,4 @@ | |||||
| import { fetchTaskTemplate, preloadAllTasks } from "@/app/api/tasks"; | |||||
| import { fetchTaskTemplateDetail, preloadAllTasks } from "@/app/api/tasks"; | |||||
| import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | ||||
| import { getServerI18n } from "@/i18n"; | import { getServerI18n } from "@/i18n"; | ||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
| @@ -27,7 +27,7 @@ const TaskTemplates: React.FC<Props> = async ({ searchParams }) => { | |||||
| preloadAllTasks(); | preloadAllTasks(); | ||||
| try { | try { | ||||
| await fetchTaskTemplate(taskTemplateId); | |||||
| await fetchTaskTemplateDetail(taskTemplateId); | |||||
| } catch (e) { | } catch (e) { | ||||
| if (e instanceof ServerFetchError && e.response?.status === 404) { | if (e instanceof ServerFetchError && e.response?.status === 404) { | ||||
| notFound(); | notFound(); | ||||
| @@ -2,6 +2,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { cache } from "react"; | import { cache } from "react"; | ||||
| import "server-only"; | import "server-only"; | ||||
| import { NewTaskTemplateFormInputs } from "./actions"; | |||||
| export interface TaskGroup { | export interface TaskGroup { | ||||
| id: number; | id: number; | ||||
| @@ -40,8 +41,8 @@ export const fetchAllTasks = cache(async () => { | |||||
| return serverFetchJson<Task[]>(`${BASE_API_URL}/tasks`); | return serverFetchJson<Task[]>(`${BASE_API_URL}/tasks`); | ||||
| }); | }); | ||||
| export const fetchTaskTemplate = cache(async (id: string) => { | |||||
| const taskTemplate = await serverFetchJson<TaskTemplate>( | |||||
| export const fetchTaskTemplateDetail = cache(async (id: string) => { | |||||
| const taskTemplate = await serverFetchJson<NewTaskTemplateFormInputs>( | |||||
| `${BASE_API_URL}/tasks/templatesDetails/${id}`, | `${BASE_API_URL}/tasks/templatesDetails/${id}`, | ||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| @@ -171,7 +171,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
| // Tab - Milestone | // Tab - Milestone | ||||
| let projectTotal = 0 | let projectTotal = 0 | ||||
| const milestonesKeys = Object.keys(data.milestones) | const milestonesKeys = Object.keys(data.milestones) | ||||
| milestonesKeys.forEach(key => { | |||||
| milestonesKeys.filter(key => Object.keys(data.taskGroups).includes(key)).forEach(key => { | |||||
| const { startDate, endDate, payments } = data.milestones[parseFloat(key)] | const { startDate, endDate, payments } = data.milestones[parseFloat(key)] | ||||
| if (!Boolean(startDate) || startDate === "Invalid Date" || !Boolean(endDate) || endDate === "Invalid Date" || new Date(startDate) > new Date(endDate)) { | if (!Boolean(startDate) || startDate === "Invalid Date" || !Boolean(endDate) || endDate === "Invalid Date" || new Date(startDate) > new Date(endDate)) { | ||||
| @@ -65,7 +65,7 @@ const Milestone: React.FC<Props> = ({ allTasks, isActive }) => { | |||||
| let hasError = false | let hasError = false | ||||
| let projectTotal = 0 | let projectTotal = 0 | ||||
| milestonesKeys.forEach(key => { | |||||
| milestonesKeys.filter(key => taskGroups.map(taskGroup => taskGroup.id).includes(parseInt(key))).forEach(key => { | |||||
| const { startDate, endDate, payments } = milestones[parseFloat(key)] | const { startDate, endDate, payments } = milestones[parseFloat(key)] | ||||
| if (new Date(startDate) > new Date(endDate) || !Boolean(startDate) || !Boolean(endDate)) { | if (new Date(startDate) > new Date(endDate) || !Boolean(startDate) || !Boolean(endDate)) { | ||||
| @@ -65,7 +65,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
| ...model, | ...model, | ||||
| [id]: { mode: GridRowModes.Edit, fieldToFocus: "description" }, | [id]: { mode: GridRowModes.Edit, fieldToFocus: "description" }, | ||||
| })); | })); | ||||
| }, []); | |||||
| }, [payments]); | |||||
| const validateRow = useCallback( | const validateRow = useCallback( | ||||
| (id: GridRowId) => { | (id: GridRowId) => { | ||||
| @@ -10,27 +10,31 @@ import TransferList from "../TransferList"; | |||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
| import Check from "@mui/icons-material/Check"; | import Check from "@mui/icons-material/Check"; | ||||
| import Close from "@mui/icons-material/Close"; | import Close from "@mui/icons-material/Close"; | ||||
| import { useRouter, useSearchParams } from "next/navigation"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import React from "react"; | import React from "react"; | ||||
| import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
| import { Task, TaskTemplate } from "@/app/api/tasks"; | import { Task, TaskTemplate } from "@/app/api/tasks"; | ||||
| import { | import { | ||||
| NewTaskTemplateFormInputs, | NewTaskTemplateFormInputs, | ||||
| fetchTaskTemplate, | |||||
| saveTaskTemplate, | saveTaskTemplate, | ||||
| } from "@/app/api/tasks/actions"; | } from "@/app/api/tasks/actions"; | ||||
| import { SubmitHandler, useFieldArray, useForm } from "react-hook-form"; | |||||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||||
| import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts"; | import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts"; | ||||
| import { Grade } from "@/app/api/grades"; | |||||
| import { intersectionWith, isEmpty } from "lodash"; | |||||
| import ResourceAllocationWrapper from "./ResourceAllocation"; | |||||
| interface Props { | interface Props { | ||||
| tasks: Task[]; | tasks: Task[]; | ||||
| defaultInputs?: TaskTemplate; | |||||
| defaultInputs?: NewTaskTemplateFormInputs; | |||||
| grades: Grade[] | |||||
| } | } | ||||
| const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||||
| const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs, grades }) => { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const searchParams = useSearchParams() | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const handleCancel = () => { | const handleCancel = () => { | ||||
| router.back(); | router.back(); | ||||
| @@ -48,57 +52,53 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||||
| const [serverError, setServerError] = React.useState(""); | const [serverError, setServerError] = React.useState(""); | ||||
| const { | |||||
| register, | |||||
| handleSubmit, | |||||
| setValue, | |||||
| watch, | |||||
| resetField, | |||||
| formState: { errors, isSubmitting }, | |||||
| } = useForm<NewTaskTemplateFormInputs>({ defaultValues: defaultInputs }); | |||||
| const currentTaskIds = watch("taskIds"); | |||||
| const formProps = useForm<NewTaskTemplateFormInputs>({ | |||||
| defaultValues: { | |||||
| taskGroups: {}, | |||||
| ...defaultInputs, | |||||
| manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade) | |||||
| ? grades.reduce((acc, grade) => { | |||||
| return { ...acc, [grade.id]: 100 / grades.length }; | |||||
| }, {}) | |||||
| : defaultInputs?.manhourPercentageByGrade, | |||||
| } | |||||
| }); | |||||
| const currentTaskGroups = formProps.watch("taskGroups"); | |||||
| const currentTaskIds = Object.values(currentTaskGroups).reduce<Task["id"][]>( | |||||
| (acc, group) => { | |||||
| return [...acc, ...group.taskIds]; | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const selectedItems = React.useMemo(() => { | const selectedItems = React.useMemo(() => { | ||||
| return items.filter((item) => currentTaskIds.includes(item.id)); | |||||
| }, [currentTaskIds, items]); | |||||
| // const [refTaskTemplate, setRefTaskTemplate] = React.useState<NewTaskTemplateFormInputs>() | |||||
| // const id = searchParams.get('id') | |||||
| // const fetchCurrentTaskTemplate = async () => { | |||||
| // try { | |||||
| // const taskTemplate = await fetchTaskTemplate(parseInt(id!!)) | |||||
| // const defaultValues = { | |||||
| // id: parseInt(id!!), | |||||
| // code: taskTemplate.code ?? null, | |||||
| // name: taskTemplate.name ?? null, | |||||
| // taskIds: taskTemplate.tasks.map(task => task.id) ?? [], | |||||
| // } | |||||
| // setRefTaskTemplate(defaultValues) | |||||
| // } catch (e) { | |||||
| // console.log(e) | |||||
| // } | |||||
| // } | |||||
| // React.useLayoutEffect(() => { | |||||
| // if (id !== null && parseInt(id) > 0) fetchCurrentTaskTemplate() | |||||
| // }, [id]) | |||||
| // React.useEffect(() => { | |||||
| // if (refTaskTemplate) { | |||||
| // setValue("taskIds", refTaskTemplate.taskIds) | |||||
| // resetField("code", { defaultValue: refTaskTemplate.code }) | |||||
| // resetField("name", { defaultValue: refTaskTemplate.name }) | |||||
| // setValue("id", refTaskTemplate.id) | |||||
| // } | |||||
| // }, [refTaskTemplate]) | |||||
| return intersectionWith( | |||||
| tasks, | |||||
| currentTaskIds, | |||||
| (task, taskId) => task.id === taskId, | |||||
| ).map((t) => ({ id: t.id, label: t.name, group: t.taskGroup })); | |||||
| }, [currentTaskIds, tasks]); | |||||
| const onSubmit: SubmitHandler<NewTaskTemplateFormInputs> = React.useCallback( | const onSubmit: SubmitHandler<NewTaskTemplateFormInputs> = React.useCallback( | ||||
| async (data) => { | async (data) => { | ||||
| try { | try { | ||||
| console.log(data) | |||||
| setServerError(""); | setServerError(""); | ||||
| let hasErrors = false | |||||
| // check the manhour allocation by stage by grade -> total = 100? | |||||
| const taskGroupKeys = Object.keys(data.taskGroups) | |||||
| if (taskGroupKeys.filter(k => data.taskGroups[k as any].percentAllocation < 0).length > 0 || | |||||
| taskGroupKeys.reduce((acc, value) => acc + data.taskGroups[value as any].percentAllocation, 0) !== 100) { | |||||
| formProps.setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" }) | |||||
| hasErrors = true | |||||
| } | |||||
| if (hasErrors) return false | |||||
| submitDialog(async () => { | submitDialog(async () => { | ||||
| const response = await saveTaskTemplate(data); | const response = await saveTaskTemplate(data); | ||||
| @@ -121,9 +121,10 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}> | |||||
| <FormProvider {...formProps}> | |||||
| <Stack component="form" onSubmit={formProps.handleSubmit(onSubmit)} gap={2}> | |||||
| {/* Task List Setup */} | |||||
| <Card> | <Card> | ||||
| {/* Task List Setup */} | |||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | ||||
| <Typography variant="overline">{t("Task List Setup")}</Typography> | <Typography variant="overline">{t("Task List Setup")}</Typography> | ||||
| <Grid | <Grid | ||||
| @@ -136,22 +137,22 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||||
| <TextField | <TextField | ||||
| label={t("Task Template Code")} | label={t("Task Template Code")} | ||||
| fullWidth | fullWidth | ||||
| {...register("code", { | |||||
| {...formProps.register("code", { | |||||
| required: t("Task template code is required"), | required: t("Task template code is required"), | ||||
| })} | })} | ||||
| error={Boolean(errors.code?.message)} | |||||
| helperText={errors.code?.message} | |||||
| error={Boolean(formProps.formState.errors.code?.message)} | |||||
| helperText={formProps.formState.errors.code?.message} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| label={t("Task Template Name")} | label={t("Task Template Name")} | ||||
| fullWidth | fullWidth | ||||
| {...register("name", { | |||||
| {...formProps.register("name", { | |||||
| required: t("Task template name is required"), | required: t("Task template name is required"), | ||||
| })} | })} | ||||
| error={Boolean(errors.name?.message)} | |||||
| helperText={errors.name?.message} | |||||
| error={Boolean(formProps.formState.errors.name?.message)} | |||||
| helperText={formProps.formState.errors.name?.message} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| @@ -159,17 +160,52 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||||
| allItems={items} | allItems={items} | ||||
| selectedItems={selectedItems} | selectedItems={selectedItems} | ||||
| onChange={(selectedTasks) => { | onChange={(selectedTasks) => { | ||||
| setValue( | |||||
| "taskIds", | |||||
| selectedTasks.map((item) => item.id), | |||||
| ); | |||||
| // formProps.setValue( | |||||
| // "taskIds", | |||||
| // selectedTasks.map((item) => item.id), | |||||
| // ); | |||||
| const newTaskGroups = selectedTasks.reduce< | |||||
| NewTaskTemplateFormInputs["taskGroups"] | |||||
| >((acc, item) => { | |||||
| if (!item.group) { | |||||
| // TODO: this should not happen (all tasks are part of a group) | |||||
| return acc; | |||||
| } | |||||
| if (!acc[item.group.id]) { | |||||
| return { | |||||
| ...acc, | |||||
| [item.group.id]: { | |||||
| taskIds: [item.id], | |||||
| percentAllocation: | |||||
| currentTaskGroups[item.group.id]?.percentAllocation || 0, | |||||
| }, | |||||
| }; | |||||
| } | |||||
| return { | |||||
| ...acc, | |||||
| [item.group.id]: { | |||||
| ...acc[item.group.id], | |||||
| taskIds: [...acc[item.group.id].taskIds, item.id], | |||||
| }, | |||||
| }; | |||||
| }, {}); | |||||
| formProps.setValue("taskGroups", newTaskGroups); | |||||
| }} | }} | ||||
| allItemsLabel={t("Task Pool")} | allItemsLabel={t("Task Pool")} | ||||
| selectedItemsLabel={t("Task List Template")} | selectedItemsLabel={t("Task List Template")} | ||||
| /> | /> | ||||
| {/* Task List Setup */} | |||||
| {/* Task List Setup */} | |||||
| </CardContent> | |||||
| </Card> | |||||
| {/* Resource Allocation */} | |||||
| <Card> | |||||
| <CardContent> | |||||
| <ResourceAllocationWrapper | |||||
| allTasks={tasks} | |||||
| grades={grades} | |||||
| /> | |||||
| </CardContent> | </CardContent> | ||||
| </Card> | </Card> | ||||
| { | { | ||||
| @@ -187,12 +223,13 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||||
| variant="contained" | variant="contained" | ||||
| startIcon={<Check />} | startIcon={<Check />} | ||||
| type="submit" | type="submit" | ||||
| disabled={isSubmitting} | |||||
| disabled={formProps.formState.isSubmitting} | |||||
| > | > | ||||
| {t("Confirm")} | {t("Confirm")} | ||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| </Stack > | </Stack > | ||||
| </FormProvider> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -1,18 +1,20 @@ | |||||
| import React from "react"; | import React from "react"; | ||||
| import CreateTaskTemplate from "./CreateTaskTemplate"; | import CreateTaskTemplate from "./CreateTaskTemplate"; | ||||
| import { fetchAllTasks, fetchTaskTemplate } from "@/app/api/tasks"; | |||||
| import { fetchAllTasks, fetchTaskTemplateDetail } from "@/app/api/tasks"; | |||||
| import { fetchGrades } from "@/app/api/grades"; | |||||
| interface Props { | interface Props { | ||||
| taskTemplateId?: string; | taskTemplateId?: string; | ||||
| } | } | ||||
| const CreateTaskTemplateWrapper: React.FC<Props> = async (props) => { | const CreateTaskTemplateWrapper: React.FC<Props> = async (props) => { | ||||
| const [tasks] = await Promise.all([ | |||||
| const [tasks, grades] = await Promise.all([ | |||||
| fetchAllTasks(), | fetchAllTasks(), | ||||
| fetchGrades(), | |||||
| ]); | ]); | ||||
| const taskTemplateInfo = props.taskTemplateId ? await fetchTaskTemplate(props.taskTemplateId) : undefined | |||||
| return <CreateTaskTemplate tasks={tasks} defaultInputs={taskTemplateInfo}/>; | |||||
| const taskTemplateInfo = props.taskTemplateId ? await fetchTaskTemplateDetail(props.taskTemplateId) : undefined | |||||
| return <CreateTaskTemplate tasks={tasks} grades={grades} defaultInputs={taskTemplateInfo}/>; | |||||
| }; | }; | ||||
| export default CreateTaskTemplateWrapper; | export default CreateTaskTemplateWrapper; | ||||
| @@ -0,0 +1,287 @@ | |||||
| import { Task, TaskGroup } from "@/app/api/tasks"; | |||||
| import { | |||||
| Box, | |||||
| Typography, | |||||
| TextField, | |||||
| Alert, | |||||
| TableContainer, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Stack, | |||||
| SxProps, | |||||
| } from "@mui/material"; | |||||
| import React, { useCallback, useMemo } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import uniqBy from "lodash/uniqBy"; | |||||
| import { Grade } from "@/app/api/grades"; | |||||
| import { percentFormatter } from "@/app/utils/formatUtil"; | |||||
| import TableCellEdit from "../TableCellEdit"; | |||||
| import { useFormContext } from "react-hook-form"; | |||||
| import { NewTaskTemplateFormInputs } from "@/app/api/tasks/actions"; | |||||
| interface Props { | |||||
| allTasks: Task[]; | |||||
| grades: Grade[]; | |||||
| } | |||||
| const leftBorderCellSx: SxProps = { | |||||
| borderLeft: "1px solid", | |||||
| borderColor: "divider", | |||||
| }; | |||||
| const rightBorderCellSx: SxProps = { | |||||
| borderRight: "1px solid", | |||||
| borderColor: "divider", | |||||
| }; | |||||
| const leftRightBorderCellSx: SxProps = { | |||||
| borderLeft: "1px solid", | |||||
| borderRight: "1px solid", | |||||
| borderColor: "divider", | |||||
| }; | |||||
| const errorCellSx: SxProps = { | |||||
| outline: "1px solid", | |||||
| outlineColor: "error.main", | |||||
| } | |||||
| const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | |||||
| const { t } = useTranslation(); | |||||
| const { watch, register, setValue, formState: { errors }, setError, clearErrors } = useFormContext<NewTaskTemplateFormInputs>(); | |||||
| const manhourPercentageByGrade = watch("manhourPercentageByGrade"); | |||||
| const totalPercentage = Math.round(Object.values(manhourPercentageByGrade).reduce( | |||||
| (acc, percent) => acc + percent, | |||||
| 0, | |||||
| ) * 100) / 100; | |||||
| const makeUpdatePercentage = useCallback( | |||||
| (gradeId: Grade["id"]) => (percentage?: number) => { | |||||
| if (percentage !== undefined) { | |||||
| const updatedManhourPercentageByGrade = { | |||||
| ...manhourPercentageByGrade, | |||||
| [gradeId]: percentage, | |||||
| } | |||||
| setValue("manhourPercentageByGrade", updatedManhourPercentageByGrade); | |||||
| const keys = Object.keys(updatedManhourPercentageByGrade) | |||||
| if (keys.filter(k => updatedManhourPercentageByGrade[k as any] < 0).length > 0 || | |||||
| keys.reduce((acc, value) => acc + updatedManhourPercentageByGrade[value as any], 0) !== 100) { | |||||
| setError("manhourPercentageByGrade", { message: "manhourPercentageByGrade value is not valid", type: "invalid" }) | |||||
| } else { | |||||
| clearErrors("manhourPercentageByGrade") | |||||
| } | |||||
| } | |||||
| }, | |||||
| [manhourPercentageByGrade, setValue], | |||||
| ); | |||||
| return ( | |||||
| <Box> | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {t("Manhour Allocation By Grade")} | |||||
| </Typography> | |||||
| <Box | |||||
| sx={(theme) => ({ | |||||
| marginBlockStart: 2, | |||||
| marginInline: -3, | |||||
| borderBottom: `1px solid ${theme.palette.divider}`, | |||||
| })} | |||||
| > | |||||
| <TableContainer sx={{ maxHeight: 440 }}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell sx={rightBorderCellSx}> | |||||
| {t("Allocation Type")} | |||||
| </TableCell> | |||||
| {grades.map((column, idx) => ( | |||||
| <TableCell key={`${column.id}${idx}`}> | |||||
| {column.name} | |||||
| </TableCell> | |||||
| ))} | |||||
| <TableCell sx={leftBorderCellSx}>{t("Total")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| <TableRow> | |||||
| <TableCell sx={rightBorderCellSx}>{t("Percentage")}</TableCell> | |||||
| {grades.map((column, idx) => ( | |||||
| <TableCellEdit | |||||
| key={`${column.id}${idx}`} | |||||
| value={manhourPercentageByGrade[column.id]} | |||||
| renderValue={(val) => val + "%"} | |||||
| onChange={makeUpdatePercentage(column.id)} | |||||
| convertValue={(inputValue) => Number(inputValue)} | |||||
| cellSx={{ backgroundColor: "primary.lightest" }} | |||||
| inputSx={{ width: "3rem" }} | |||||
| error={manhourPercentageByGrade[column.id] < 0} | |||||
| /> | |||||
| ))} | |||||
| <TableCell sx={{ ...(totalPercentage === 100 && leftBorderCellSx), ...(totalPercentage !== 100 && { ...errorCellSx, borderRight: "1px solid", borderColor: "error.main" }) }}> | |||||
| {totalPercentage + "%"} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| </Box> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||||
| const { t } = useTranslation(); | |||||
| const { watch, setValue, clearErrors, setError } = useFormContext<NewTaskTemplateFormInputs>(); | |||||
| const currentTaskGroups = watch("taskGroups"); | |||||
| const taskGroups = useMemo( | |||||
| () => | |||||
| uniqBy( | |||||
| allTasks.reduce<TaskGroup[]>((acc, task) => { | |||||
| if (currentTaskGroups[task.taskGroup.id]) { | |||||
| return [...acc, task.taskGroup]; | |||||
| } | |||||
| return acc; | |||||
| }, []), | |||||
| "id", | |||||
| ), | |||||
| [allTasks, currentTaskGroups], | |||||
| ); | |||||
| const manhourPercentageByGrade = watch("manhourPercentageByGrade"); | |||||
| const makeUpdatePercentage = useCallback( | |||||
| (taskGroupId: TaskGroup["id"]) => (percentage?: number) => { | |||||
| console.log(percentage) | |||||
| if (percentage !== undefined) { | |||||
| const updatedTaskGroups = { | |||||
| ...currentTaskGroups, | |||||
| [taskGroupId]: { | |||||
| ...currentTaskGroups[taskGroupId], | |||||
| percentAllocation: percentage, | |||||
| }, | |||||
| } | |||||
| console.log(updatedTaskGroups) | |||||
| setValue("taskGroups", updatedTaskGroups); | |||||
| const keys = Object.keys(updatedTaskGroups) | |||||
| if (keys.filter(k => updatedTaskGroups[k as any].percentAllocation < 0).length > 0 || | |||||
| keys.reduce((acc, value) => acc + updatedTaskGroups[value as any].percentAllocation, 0) !== 100) { | |||||
| setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" }) | |||||
| } else { | |||||
| clearErrors("taskGroups") | |||||
| } | |||||
| } | |||||
| }, | |||||
| [currentTaskGroups, setValue], | |||||
| ); | |||||
| return ( | |||||
| <Box> | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {t("Manhour Allocation By Stage By Grade")} | |||||
| </Typography> | |||||
| <Box | |||||
| sx={(theme) => ({ | |||||
| marginBlockStart: 2, | |||||
| marginInline: -3, | |||||
| borderBottom: `1px solid ${theme.palette.divider}`, | |||||
| })} | |||||
| > | |||||
| <TableContainer sx={{ maxHeight: 440 }}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>{t("Stage")}</TableCell> | |||||
| <TableCell sx={leftBorderCellSx}>{t("Task Count")}</TableCell> | |||||
| <TableCell colSpan={2} sx={leftRightBorderCellSx}> | |||||
| {t("Total Manhour")} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {taskGroups.map((tg, idx) => ( | |||||
| <TableRow key={`${tg.id}${idx}`}> | |||||
| <TableCell>{tg.name}</TableCell> | |||||
| <TableCell sx={leftBorderCellSx}> | |||||
| {currentTaskGroups[tg.id].taskIds.length} | |||||
| </TableCell> | |||||
| <TableCellEdit | |||||
| value={currentTaskGroups[tg.id].percentAllocation} | |||||
| // renderValue={(val) => percentFormatter.format(val)} | |||||
| renderValue={(val) => val + "%"} | |||||
| onChange={makeUpdatePercentage(tg.id)} | |||||
| convertValue={(inputValue) => Number(inputValue)} | |||||
| cellSx={{ | |||||
| backgroundColor: "primary.lightest", | |||||
| ...(currentTaskGroups[tg.id].percentAllocation < 0 && { ...errorCellSx, borderBottom: "0px", borderRight: "1px solid", borderColor: "error.main"}) | |||||
| }} | |||||
| inputSx={{ width: "3rem" }} | |||||
| error={currentTaskGroups[tg.id].percentAllocation < 0} | |||||
| /> | |||||
| </TableRow> | |||||
| ))} | |||||
| <TableRow> | |||||
| <TableCell>{t("Total")}</TableCell> | |||||
| <TableCell sx={leftBorderCellSx}> | |||||
| {Object.values(currentTaskGroups).reduce( | |||||
| (acc, tg) => acc + tg.taskIds.length, | |||||
| 0, | |||||
| )} | |||||
| </TableCell> | |||||
| <TableCell sx={{ | |||||
| ...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) === 100 && leftBorderCellSx), | |||||
| ...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) !== 100 && { ...errorCellSx, borderRight: "1px solid", borderColor: "error.main"}) | |||||
| }} | |||||
| > | |||||
| {percentFormatter.format( | |||||
| Object.values(currentTaskGroups).reduce( | |||||
| (acc, tg) => acc + tg.percentAllocation / 100, | |||||
| 0, | |||||
| ), | |||||
| )} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| </Box> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| const NoTaskState: React.FC = () => { | |||||
| const { t } = useTranslation(); | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {t("Task Breakdown")} | |||||
| </Typography> | |||||
| <Alert severity="warning"> | |||||
| {t('Please add some tasks first!')} | |||||
| </Alert> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| const ResourceAllocationWrapper: React.FC<Props> = (props) => { | |||||
| const { getValues } = useFormContext<NewTaskTemplateFormInputs>(); | |||||
| if (Object.keys(getValues("taskGroups")).length === 0) { | |||||
| return <NoTaskState />; | |||||
| } | |||||
| return ( | |||||
| <Stack spacing={4}> | |||||
| <ResourceAllocationByGrade {...props} /> | |||||
| <ResourceAllocationByStage {...props} /> | |||||
| </Stack> | |||||
| ); | |||||
| }; | |||||
| export default ResourceAllocationWrapper; | |||||