| @@ -1,4 +1,4 @@ | |||
| import { fetchTaskTemplate, preloadAllTasks } from "@/app/api/tasks"; | |||
| import { fetchTaskTemplateDetail, preloadAllTasks } from "@/app/api/tasks"; | |||
| import CreateTaskTemplate from "@/components/CreateTaskTemplate"; | |||
| import { getServerI18n } from "@/i18n"; | |||
| import Typography from "@mui/material/Typography"; | |||
| @@ -27,7 +27,7 @@ const TaskTemplates: React.FC<Props> = async ({ searchParams }) => { | |||
| preloadAllTasks(); | |||
| try { | |||
| await fetchTaskTemplate(taskTemplateId); | |||
| await fetchTaskTemplateDetail(taskTemplateId); | |||
| } catch (e) { | |||
| if (e instanceof ServerFetchError && e.response?.status === 404) { | |||
| notFound(); | |||
| @@ -2,6 +2,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { cache } from "react"; | |||
| import "server-only"; | |||
| import { NewTaskTemplateFormInputs } from "./actions"; | |||
| export interface TaskGroup { | |||
| id: number; | |||
| @@ -40,8 +41,8 @@ export const fetchAllTasks = cache(async () => { | |||
| 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}`, | |||
| { | |||
| method: "GET", | |||
| @@ -171,7 +171,7 @@ const CreateProject: React.FC<Props> = ({ | |||
| // Tab - Milestone | |||
| let projectTotal = 0 | |||
| 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)] | |||
| 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 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)] | |||
| if (new Date(startDate) > new Date(endDate) || !Boolean(startDate) || !Boolean(endDate)) { | |||
| @@ -65,7 +65,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
| ...model, | |||
| [id]: { mode: GridRowModes.Edit, fieldToFocus: "description" }, | |||
| })); | |||
| }, []); | |||
| }, [payments]); | |||
| const validateRow = useCallback( | |||
| (id: GridRowId) => { | |||
| @@ -10,27 +10,31 @@ import TransferList from "../TransferList"; | |||
| import Button from "@mui/material/Button"; | |||
| import Check from "@mui/icons-material/Check"; | |||
| import Close from "@mui/icons-material/Close"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { useRouter } from "next/navigation"; | |||
| import React from "react"; | |||
| import Stack from "@mui/material/Stack"; | |||
| import { Task, TaskTemplate } from "@/app/api/tasks"; | |||
| import { | |||
| NewTaskTemplateFormInputs, | |||
| fetchTaskTemplate, | |||
| saveTaskTemplate, | |||
| } 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 { Grade } from "@/app/api/grades"; | |||
| import { intersectionWith, isEmpty } from "lodash"; | |||
| import ResourceAllocationWrapper from "./ResourceAllocation"; | |||
| interface Props { | |||
| 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 searchParams = useSearchParams() | |||
| const router = useRouter(); | |||
| const handleCancel = () => { | |||
| router.back(); | |||
| @@ -48,57 +52,53 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||
| 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(() => { | |||
| 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( | |||
| async (data) => { | |||
| try { | |||
| console.log(data) | |||
| 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 () => { | |||
| const response = await saveTaskTemplate(data); | |||
| @@ -121,9 +121,10 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||
| return ( | |||
| <> | |||
| <Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}> | |||
| <FormProvider {...formProps}> | |||
| <Stack component="form" onSubmit={formProps.handleSubmit(onSubmit)} gap={2}> | |||
| {/* Task List Setup */} | |||
| <Card> | |||
| {/* Task List Setup */} | |||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
| <Typography variant="overline">{t("Task List Setup")}</Typography> | |||
| <Grid | |||
| @@ -136,22 +137,22 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||
| <TextField | |||
| label={t("Task Template Code")} | |||
| fullWidth | |||
| {...register("code", { | |||
| {...formProps.register("code", { | |||
| 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 item xs={6}> | |||
| <TextField | |||
| label={t("Task Template Name")} | |||
| fullWidth | |||
| {...register("name", { | |||
| {...formProps.register("name", { | |||
| 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> | |||
| @@ -159,17 +160,52 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||
| allItems={items} | |||
| selectedItems={selectedItems} | |||
| 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")} | |||
| selectedItemsLabel={t("Task List Template")} | |||
| /> | |||
| {/* Task List Setup */} | |||
| {/* Task List Setup */} | |||
| </CardContent> | |||
| </Card> | |||
| {/* Resource Allocation */} | |||
| <Card> | |||
| <CardContent> | |||
| <ResourceAllocationWrapper | |||
| allTasks={tasks} | |||
| grades={grades} | |||
| /> | |||
| </CardContent> | |||
| </Card> | |||
| { | |||
| @@ -187,12 +223,13 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| disabled={isSubmitting} | |||
| disabled={formProps.formState.isSubmitting} | |||
| > | |||
| {t("Confirm")} | |||
| </Button> | |||
| </Stack> | |||
| </Stack > | |||
| </FormProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -1,18 +1,20 @@ | |||
| import React from "react"; | |||
| 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 { | |||
| taskTemplateId?: string; | |||
| } | |||
| const CreateTaskTemplateWrapper: React.FC<Props> = async (props) => { | |||
| const [tasks] = await Promise.all([ | |||
| const [tasks, grades] = await Promise.all([ | |||
| 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; | |||
| @@ -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; | |||