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