Просмотр исходного кода

update project & task template

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui 1 год назад
Родитель
Сommit
7636743bee
8 измененных файлов: 404 добавлений и 77 удалений
  1. +2
    -2
      src/app/(main)/tasks/edit/page.tsx
  2. +3
    -2
      src/app/api/tasks/index.ts
  3. +1
    -1
      src/components/CreateProject/CreateProject.tsx
  4. +1
    -1
      src/components/CreateProject/Milestone.tsx
  5. +1
    -1
      src/components/CreateProject/MilestoneSection.tsx
  6. +103
    -66
      src/components/CreateTaskTemplate/CreateTaskTemplate.tsx
  7. +6
    -4
      src/components/CreateTaskTemplate/CreateTaskTemplateWrapper.tsx
  8. +287
    -0
      src/components/CreateTaskTemplate/ResourceAllocation.tsx

+ 2
- 2
src/app/(main)/tasks/edit/page.tsx Просмотреть файл

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


+ 3
- 2
src/app/api/tasks/index.ts Просмотреть файл

@@ -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",


+ 1
- 1
src/components/CreateProject/CreateProject.tsx Просмотреть файл

@@ -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)) {


+ 1
- 1
src/components/CreateProject/Milestone.tsx Просмотреть файл

@@ -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)) {


+ 1
- 1
src/components/CreateProject/MilestoneSection.tsx Просмотреть файл

@@ -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) => {


+ 103
- 66
src/components/CreateTaskTemplate/CreateTaskTemplate.tsx Просмотреть файл

@@ -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>
</> </>
); );
}; };


+ 6
- 4
src/components/CreateTaskTemplate/CreateTaskTemplateWrapper.tsx Просмотреть файл

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

+ 287
- 0
src/components/CreateTaskTemplate/ResourceAllocation.tsx Просмотреть файл

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

Загрузка…
Отмена
Сохранить