@@ -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; |