|
@@ -9,22 +9,40 @@ import { |
|
|
ListItemText, |
|
|
ListItemText, |
|
|
TextField, |
|
|
TextField, |
|
|
} from "@mui/material"; |
|
|
} from "@mui/material"; |
|
|
import { useState, useCallback, useEffect } from "react"; |
|
|
|
|
|
|
|
|
import { useState, useCallback, useEffect, useMemo } from "react"; |
|
|
import { useTranslation } from "react-i18next"; |
|
|
import { useTranslation } from "react-i18next"; |
|
|
import { Props as ResourceMilestoneProps } from "./ResourceMilestone"; |
|
|
import { Props as ResourceMilestoneProps } from "./ResourceMilestone"; |
|
|
|
|
|
import StyledDataGrid from "../StyledDataGrid"; |
|
|
|
|
|
import { useForm, useFormContext } from "react-hook-form"; |
|
|
|
|
|
import { GridColDef, GridRowModel, useGridApiRef } from "@mui/x-data-grid"; |
|
|
|
|
|
import { |
|
|
|
|
|
CreateProjectInputs, |
|
|
|
|
|
ManhourAllocation, |
|
|
|
|
|
} from "@/app/api/projects/actions"; |
|
|
|
|
|
import isEmpty from "lodash/isEmpty"; |
|
|
|
|
|
import _reduce from "lodash/reduce"; |
|
|
|
|
|
|
|
|
|
|
|
const mockGrades = [1, 2, 3, 4, 5]; |
|
|
|
|
|
|
|
|
interface Props { |
|
|
interface Props { |
|
|
tasks: Task[]; |
|
|
tasks: Task[]; |
|
|
defaultManhourBreakdownByGrade: ResourceMilestoneProps["defaultManhourBreakdownByGrade"]; |
|
|
|
|
|
onSetManhours: (hours: number, taskId: Task["id"]) => void; |
|
|
|
|
|
onAllocateManhours: () => void; |
|
|
|
|
|
|
|
|
manhourBreakdownByGrade: ResourceMilestoneProps["defaultManhourBreakdownByGrade"]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type Row = ManhourAllocation & { id: "manhourAllocation" }; |
|
|
|
|
|
|
|
|
|
|
|
const parseValidManhours = (value: number | string): number => { |
|
|
|
|
|
const inputNumber = Number(value); |
|
|
|
|
|
return isNaN(inputNumber) || inputNumber < 0 ? 0 : inputNumber; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const ResourceSection: React.FC<Props> = ({ |
|
|
const ResourceSection: React.FC<Props> = ({ |
|
|
tasks, |
|
|
tasks, |
|
|
onAllocateManhours, |
|
|
|
|
|
onSetManhours, |
|
|
|
|
|
defaultManhourBreakdownByGrade, |
|
|
|
|
|
|
|
|
manhourBreakdownByGrade = mockGrades.reduce< |
|
|
|
|
|
NonNullable<Props["manhourBreakdownByGrade"]> |
|
|
|
|
|
>((acc, grade) => { |
|
|
|
|
|
return { ...acc, [grade]: 1 }; |
|
|
|
|
|
}, {}), |
|
|
}) => { |
|
|
}) => { |
|
|
const { t } = useTranslation(); |
|
|
const { t } = useTranslation(); |
|
|
const [selectedTaskId, setSelectedTaskId] = useState(tasks[0].id); |
|
|
const [selectedTaskId, setSelectedTaskId] = useState(tasks[0].id); |
|
@@ -40,10 +58,116 @@ const ResourceSection: React.FC<Props> = ({ |
|
|
setSelectedTaskId(tasks[0].id); |
|
|
setSelectedTaskId(tasks[0].id); |
|
|
}, [tasks]); |
|
|
}, [tasks]); |
|
|
|
|
|
|
|
|
|
|
|
const { getValues, setValue } = useFormContext<CreateProjectInputs>(); |
|
|
|
|
|
|
|
|
|
|
|
const updateTaskAllocations = useCallback( |
|
|
|
|
|
(newAllocations: ManhourAllocation) => { |
|
|
|
|
|
setValue("tasks", { |
|
|
|
|
|
...getValues("tasks"), |
|
|
|
|
|
[selectedTaskId]: { |
|
|
|
|
|
manhourAllocation: newAllocations, |
|
|
|
|
|
}, |
|
|
|
|
|
}); |
|
|
|
|
|
}, |
|
|
|
|
|
[getValues, selectedTaskId, setValue], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const gridApiRef = useGridApiRef(); |
|
|
|
|
|
const columns = useMemo(() => { |
|
|
|
|
|
return mockGrades.map<GridColDef>((grade) => ({ |
|
|
|
|
|
field: grade.toString(), |
|
|
|
|
|
editable: true, |
|
|
|
|
|
sortable: false, |
|
|
|
|
|
width: 120, |
|
|
|
|
|
headerName: t("Grade {{grade}}", { grade }), |
|
|
|
|
|
type: "number", |
|
|
|
|
|
valueParser: parseValidManhours, |
|
|
|
|
|
valueFormatter(params) { |
|
|
|
|
|
return Number(params.value).toFixed(2); |
|
|
|
|
|
}, |
|
|
|
|
|
})); |
|
|
|
|
|
}, [t]); |
|
|
|
|
|
|
|
|
|
|
|
const rows = useMemo<Row[]>(() => { |
|
|
|
|
|
const initialAllocation = |
|
|
|
|
|
getValues("tasks")[selectedTaskId].manhourAllocation; |
|
|
|
|
|
if (!isEmpty(initialAllocation)) { |
|
|
|
|
|
return [{ ...initialAllocation, id: "manhourAllocation" }]; |
|
|
|
|
|
} |
|
|
|
|
|
return [ |
|
|
|
|
|
mockGrades.reduce( |
|
|
|
|
|
(acc, grade) => { |
|
|
|
|
|
return { ...acc, [grade]: 0 }; |
|
|
|
|
|
}, |
|
|
|
|
|
{ id: "manhourAllocation" }, |
|
|
|
|
|
), |
|
|
|
|
|
]; |
|
|
|
|
|
}, [getValues, selectedTaskId]); |
|
|
|
|
|
|
|
|
|
|
|
const initialManhours = useMemo(() => { |
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars |
|
|
|
|
|
const { id, ...allocations } = rows[0]; |
|
|
|
|
|
return Object.values(allocations).reduce((acc, hours) => acc + hours, 0); |
|
|
|
|
|
}, [rows]); |
|
|
|
|
|
|
|
|
|
|
|
const { |
|
|
|
|
|
register, |
|
|
|
|
|
reset, |
|
|
|
|
|
getValues: getManhourFormValues, |
|
|
|
|
|
setValue: setManhourFormValue, |
|
|
|
|
|
} = useForm<{ manhour: number }>({ |
|
|
|
|
|
defaultValues: { |
|
|
|
|
|
manhour: initialManhours, |
|
|
|
|
|
}, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Reset man hour input when task changes |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
reset({ manhour: initialManhours }); |
|
|
|
|
|
}, [initialManhours, reset, selectedTaskId]); |
|
|
|
|
|
|
|
|
|
|
|
const updateAllocation = useCallback(() => { |
|
|
|
|
|
const inputHour = getManhourFormValues("manhour"); |
|
|
|
|
|
const ratioSum = Object.values(manhourBreakdownByGrade).reduce( |
|
|
|
|
|
(acc, ratio) => acc + ratio, |
|
|
|
|
|
0, |
|
|
|
|
|
); |
|
|
|
|
|
const newAllocations = _reduce( |
|
|
|
|
|
manhourBreakdownByGrade, |
|
|
|
|
|
(acc, value, key) => { |
|
|
|
|
|
return { ...acc, [key]: (inputHour / ratioSum) * value }; |
|
|
|
|
|
}, |
|
|
|
|
|
{}, |
|
|
|
|
|
); |
|
|
|
|
|
gridApiRef.current.updateRows([ |
|
|
|
|
|
{ id: "manhourAllocation", ...newAllocations }, |
|
|
|
|
|
]); |
|
|
|
|
|
updateTaskAllocations(newAllocations); |
|
|
|
|
|
}, [ |
|
|
|
|
|
getManhourFormValues, |
|
|
|
|
|
gridApiRef, |
|
|
|
|
|
manhourBreakdownByGrade, |
|
|
|
|
|
updateTaskAllocations, |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
const processRowUpdate = useCallback( |
|
|
|
|
|
(newRow: GridRowModel) => { |
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars |
|
|
|
|
|
const { id: rowId, ...newAllocations } = newRow; |
|
|
|
|
|
const totalHours = Object.values( |
|
|
|
|
|
newAllocations as ManhourAllocation, |
|
|
|
|
|
).reduce<number>((acc, hour) => acc + hour, 0); |
|
|
|
|
|
setManhourFormValue("manhour", totalHours); |
|
|
|
|
|
updateTaskAllocations(newAllocations); |
|
|
|
|
|
return newRow; |
|
|
|
|
|
}, |
|
|
|
|
|
[setManhourFormValue, updateTaskAllocations], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<Box marginBlock={4}> |
|
|
<Box marginBlock={4}> |
|
|
<Typography variant="overline" display="block" marginBlockEnd={1}> |
|
|
<Typography variant="overline" display="block" marginBlockEnd={1}> |
|
|
{t("Resource")} |
|
|
|
|
|
|
|
|
{t("Task Breakdown")} |
|
|
</Typography> |
|
|
</Typography> |
|
|
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> |
|
|
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> |
|
|
<Grid item xs={6}> |
|
|
<Grid item xs={6}> |
|
@@ -64,7 +188,35 @@ const ResourceSection: React.FC<Props> = ({ |
|
|
</Paper> |
|
|
</Paper> |
|
|
</Grid> |
|
|
</Grid> |
|
|
<Grid item xs={6}> |
|
|
<Grid item xs={6}> |
|
|
<TextField label={t("Mahours Allocated to Task")} fullWidth /> |
|
|
|
|
|
|
|
|
<TextField |
|
|
|
|
|
label={t("Mahours Allocated to Task")} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
type="number" |
|
|
|
|
|
{...register("manhour", { |
|
|
|
|
|
valueAsNumber: true, |
|
|
|
|
|
onBlur: updateAllocation, |
|
|
|
|
|
})} |
|
|
|
|
|
/> |
|
|
|
|
|
<Paper |
|
|
|
|
|
elevation={2} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
marginBlockStart: 2, |
|
|
|
|
|
".MuiDataGrid-root .MuiDataGrid-columnHeader:focus-within": { |
|
|
|
|
|
outlineOffset: -2, |
|
|
|
|
|
}, |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<StyledDataGrid |
|
|
|
|
|
apiRef={gridApiRef} |
|
|
|
|
|
disableColumnMenu |
|
|
|
|
|
hideFooter |
|
|
|
|
|
disableRowSelectionOnClick |
|
|
|
|
|
rows={rows} |
|
|
|
|
|
columns={columns} |
|
|
|
|
|
processRowUpdate={processRowUpdate} |
|
|
|
|
|
sx={{ paddingBlockEnd: 2 }} |
|
|
|
|
|
/> |
|
|
|
|
|
</Paper> |
|
|
</Grid> |
|
|
</Grid> |
|
|
</Grid> |
|
|
</Grid> |
|
|
</Box> |
|
|
</Box> |
|
|