@@ -144,22 +144,48 @@ const CreateProject: React.FC<Props> = ({ | |||
// detect errors | |||
let hasErrors = false | |||
// Tab - Staff Allocation and Resource | |||
if (data.totalManhour === null || data.totalManhour <= 0) { | |||
formProps.setError("totalManhour", { message: "totalManhour value is not valid", type: "required" }) | |||
setTabIndex(2) | |||
hasErrors = true | |||
} | |||
const manhourPercentageByGradeKeys = Object.keys(data.manhourPercentageByGrade) | |||
if (manhourPercentageByGradeKeys.filter(k => data.manhourPercentageByGrade[k as any] < 0).length > 0 || | |||
manhourPercentageByGradeKeys.reduce((acc, value) => acc + data.manhourPercentageByGrade[value as any], 0) !== 1) { | |||
manhourPercentageByGradeKeys.reduce((acc, value) => acc + data.manhourPercentageByGrade[value as any], 0) !== 100) { | |||
formProps.setError("manhourPercentageByGrade", { message: "manhourPercentageByGrade value is not valid", type: "invalid" }) | |||
setTabIndex(2) | |||
hasErrors = true | |||
} | |||
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) !== 1) { | |||
formProps.setError("taskGroups", {message: "Task Groups value is not invalid", type: "invalid"}) | |||
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" }) | |||
setTabIndex(2) | |||
hasErrors = true | |||
} | |||
// Tab - Milestone | |||
let projectTotal = 0 | |||
const milestonesKeys = Object.keys(data.milestones) | |||
milestonesKeys.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)) { | |||
formProps.setError("milestones", {message: "milestones is not valid", type: "invalid"}) | |||
setTabIndex(3) | |||
hasErrors = true | |||
} | |||
projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0) | |||
}) | |||
if (projectTotal !== data.expectedProjectFee) { | |||
formProps.setError("milestones", {message: "milestones is not valid", type: "invalid"}) | |||
setTabIndex(3) | |||
hasErrors = true | |||
} | |||
@@ -245,7 +271,7 @@ const CreateProject: React.FC<Props> = ({ | |||
// manhourPercentageByGrade should have a sensible default | |||
manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade) | |||
? grades.reduce((acc, grade) => { | |||
return { ...acc, [grade.id]: 1 / grades.length }; | |||
return { ...acc, [grade.id]: 100 / grades.length }; | |||
}, {}) | |||
: defaultInputs?.manhourPercentageByGrade, | |||
}, | |||
@@ -4,7 +4,7 @@ import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import { useTranslation } from "react-i18next"; | |||
import Button from "@mui/material/Button"; | |||
import React, { useCallback, useMemo, useState } from "react"; | |||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
import CardActions from "@mui/material/CardActions"; | |||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import { | |||
@@ -29,7 +29,7 @@ export interface Props { | |||
const Milestone: React.FC<Props> = ({ allTasks, isActive }) => { | |||
const { t } = useTranslation(); | |||
const { watch } = useFormContext<CreateProjectInputs>(); | |||
const { watch, setError, clearErrors } = useFormContext<CreateProjectInputs>(); | |||
const currentTaskGroups = watch("taskGroups"); | |||
const taskGroups = useMemo( | |||
() => | |||
@@ -57,6 +57,35 @@ const Milestone: React.FC<Props> = ({ allTasks, isActive }) => { | |||
[], | |||
); | |||
// handle error checking | |||
const milestones = watch("milestones") | |||
const expectedTotalFee = watch("expectedProjectFee"); | |||
useEffect(() => { | |||
const milestonesKeys = Object.keys(milestones) | |||
let hasError = false | |||
let projectTotal = 0 | |||
milestonesKeys.forEach(key => { | |||
const { startDate, endDate, payments } = milestones[parseFloat(key)] | |||
if (new Date(startDate) > new Date(endDate) || !Boolean(startDate) || !Boolean(endDate)) { | |||
hasError = true | |||
} | |||
projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0) | |||
}) | |||
if (projectTotal !== expectedTotalFee) { | |||
hasError = true | |||
} | |||
// console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0)) | |||
if (hasError) { | |||
setError("milestones", {message: "milestones is not valid", type: "invalid"}) | |||
} else { | |||
clearErrors("milestones") | |||
} | |||
}, [milestones]) | |||
return ( | |||
<> | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
@@ -26,7 +26,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
import dayjs from "dayjs"; | |||
import "dayjs/locale/zh-hk"; | |||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { Controller, useFormContext } from "react-hook-form"; | |||
import { useTranslation } from "react-i18next"; | |||
import StyledDataGrid from "../StyledDataGrid"; | |||
import { INPUT_DATE_FORMAT, moneyFormatter } from "@/app/utils/formatUtil"; | |||
@@ -57,7 +57,9 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
const apiRef = useGridApiRef(); | |||
const addRow = useCallback(() => { | |||
const id = Date.now(); | |||
// const id = Date.now(); | |||
const minId = Math.min(...payments.map((payment) => payment.id!!)); | |||
const id = minId >= 0 ? -1 : minId - 1 | |||
setPayments((p) => [...p, { id, _isNew: true }]); | |||
setRowModesModel((model) => ({ | |||
...model, | |||
@@ -239,21 +241,26 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs> | |||
<FormControl fullWidth> | |||
<DatePicker | |||
label={t("Stage Start Date")} | |||
value={startDate ? dayjs(startDate) : null} | |||
onChange={(date) => { | |||
if (!date) return; | |||
const milestones = getValues("milestones"); | |||
setValue("milestones", { | |||
...milestones, | |||
[taskGroupId]: { | |||
...milestones[taskGroupId], | |||
startDate: date.format(INPUT_DATE_FORMAT), | |||
}, | |||
}); | |||
}} | |||
/> | |||
<DatePicker | |||
label={t("Stage Start Date")} | |||
value={startDate ? dayjs(startDate) : null} | |||
onChange={(date) => { | |||
if (!date) return; | |||
const milestones = getValues("milestones"); | |||
setValue("milestones", { | |||
...milestones, | |||
[taskGroupId]: { | |||
...milestones[taskGroupId], | |||
startDate: date.format(INPUT_DATE_FORMAT), | |||
}, | |||
}); | |||
}} | |||
slotProps={{ | |||
textField: { | |||
error: startDate === "Invalid Date" || new Date(startDate) > new Date(endDate) || (Boolean(formState.errors.milestones) && !Boolean(startDate)), | |||
}, | |||
}} | |||
/> | |||
</FormControl> | |||
</Grid> | |||
<Grid item xs> | |||
@@ -272,6 +279,11 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
}, | |||
}); | |||
}} | |||
slotProps={{ | |||
textField: { | |||
error: endDate === "Invalid Date" || new Date(startDate) > new Date(endDate) || (Boolean(formState.errors.milestones) && !Boolean(endDate)), | |||
}, | |||
}} | |||
/> | |||
</FormControl> | |||
</Grid> | |||
@@ -2,7 +2,7 @@ import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
import { TaskGroup } from "@/app/api/tasks"; | |||
import { moneyFormatter } from "@/app/utils/formatUtil"; | |||
import { Divider, Stack, Typography } from "@mui/material"; | |||
import React, { useCallback, useEffect, useMemo } from "react"; | |||
import React from "react"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { useTranslation } from "react-i18next"; | |||
@@ -12,21 +12,12 @@ interface Props { | |||
const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => { | |||
const { t } = useTranslation(); | |||
const { watch, setError, clearErrors } = useFormContext<CreateProjectInputs>(); | |||
const { watch } = useFormContext<CreateProjectInputs>(); | |||
const milestones = watch("milestones"); | |||
const expectedTotalFee = watch("expectedProjectFee"); | |||
let projectTotal = 0; | |||
useEffect(() => { | |||
console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseInt(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0)) | |||
if (Object.keys(milestones).reduce((acc, key) => acc + milestones[parseInt(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0) !== expectedTotalFee) { | |||
setError("milestones", {message: "project total is not valid", type: "invalid"}) | |||
} else { | |||
clearErrors("milestones") | |||
} | |||
}, [milestones]) | |||
return ( | |||
<Stack spacing={1}> | |||
{taskGroups.map((group, index) => { | |||
@@ -62,10 +62,10 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | |||
const manhourPercentageByGrade = watch("manhourPercentageByGrade"); | |||
const totalManhour = watch("totalManhour"); | |||
const totalPercentage = Object.values(manhourPercentageByGrade).reduce( | |||
const totalPercentage = Math.round(Object.values(manhourPercentageByGrade).reduce( | |||
(acc, percent) => acc + percent, | |||
0, | |||
); | |||
) * 100) / 100; | |||
const makeUpdatePercentage = useCallback( | |||
(gradeId: Grade["id"]) => (percentage?: number) => { | |||
@@ -78,7 +78,7 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | |||
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) !== 1) { | |||
keys.reduce((acc, value) => acc + updatedManhourPercentageByGrade[value as any], 0) !== 100) { | |||
setError("manhourPercentageByGrade", {message: "manhourPercentageByGrade value is not valid", type: "invalid"}) | |||
} else { | |||
clearErrors("manhourPercentageByGrade") | |||
@@ -134,7 +134,8 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | |||
<TableCellEdit | |||
key={`${column.id}${idx}`} | |||
value={manhourPercentageByGrade[column.id]} | |||
renderValue={(val) => percentFormatter.format(val)} | |||
renderValue={(val) => val + "%"} | |||
// renderValue={(val) => percentFormatter.format(val)} | |||
onChange={makeUpdatePercentage(column.id)} | |||
convertValue={(inputValue) => Number(inputValue)} | |||
cellSx={{ backgroundColor: "primary.lightest" }} | |||
@@ -142,8 +143,9 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | |||
error={manhourPercentageByGrade[column.id] < 0} | |||
/> | |||
))} | |||
<TableCell sx={{ ...(totalPercentage === 1 && leftBorderCellSx), ...(totalPercentage !== 1 && {...errorCellSx, borderRight: "1px solid", borderColor: "error.main"})}}> | |||
{percentFormatter.format(totalPercentage)} | |||
<TableCell sx={{ ...(totalPercentage === 100 && leftBorderCellSx), ...(totalPercentage !== 100 && {...errorCellSx, borderRight: "1px solid", borderColor: "error.main"})}}> | |||
{totalPercentage + "%"} | |||
{/* {percentFormatter.format(totalPercentage)} */} | |||
</TableCell> | |||
</TableRow> | |||
<TableRow> | |||
@@ -151,7 +153,7 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => { | |||
{grades.map((column, idx) => ( | |||
<TableCell key={`${column.id}${idx}`}> | |||
{manhourFormatter.format( | |||
manhourPercentageByGrade[column.id] * totalManhour, | |||
manhourPercentageByGrade[column.id] / 100 * totalManhour, | |||
)} | |||
</TableCell> | |||
))} | |||
@@ -203,7 +205,7 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||
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) !== 1) { | |||
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") | |||
@@ -250,7 +252,8 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||
</TableCell> | |||
<TableCellEdit | |||
value={currentTaskGroups[tg.id].percentAllocation} | |||
renderValue={(val) => percentFormatter.format(val)} | |||
// renderValue={(val) => percentFormatter.format(val)} | |||
renderValue={(val) => val + "%"} | |||
onChange={makeUpdatePercentage(tg.id)} | |||
convertValue={(inputValue) => Number(inputValue)} | |||
cellSx={{ | |||
@@ -261,7 +264,7 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||
/> | |||
<TableCell sx={rightBorderCellSx}> | |||
{manhourFormatter.format( | |||
currentTaskGroups[tg.id].percentAllocation * totalManhour, | |||
currentTaskGroups[tg.id].percentAllocation / 100 * totalManhour, | |||
)} | |||
</TableCell> | |||
{grades.map((column, idx) => { | |||
@@ -270,7 +273,7 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||
return ( | |||
<TableCell key={`${column.id}${idx}`}> | |||
{manhourFormatter.format( | |||
manhourPercentageByGrade[column.id] * stageHours, | |||
manhourPercentageByGrade[column.id] / 100 * stageHours, | |||
)} | |||
</TableCell> | |||
); | |||
@@ -286,13 +289,13 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||
)} | |||
</TableCell> | |||
<TableCell sx={{ | |||
...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) === 1 && leftBorderCellSx), | |||
...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) !== 1 && errorCellSx) | |||
...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) === 100 && leftBorderCellSx), | |||
...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) !== 100 && errorCellSx) | |||
}} | |||
> | |||
{percentFormatter.format( | |||
Object.values(currentTaskGroups).reduce( | |||
(acc, tg) => acc + tg.percentAllocation, | |||
(acc, tg) => acc + tg.percentAllocation / 100, | |||
0, | |||
), | |||
)} | |||
@@ -300,7 +303,7 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||
<TableCell sx={rightBorderCellSx}> | |||
{manhourFormatter.format( | |||
Object.values(currentTaskGroups).reduce( | |||
(acc, tg) => acc + tg.percentAllocation * totalManhour, | |||
(acc, tg) => acc + tg.percentAllocation / 100 * totalManhour, | |||
0, | |||
), | |||
)} | |||
@@ -309,7 +312,7 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { | |||
const hours = Object.values(currentTaskGroups).reduce( | |||
(acc, tg) => | |||
acc + | |||
tg.percentAllocation * | |||
tg.percentAllocation / 100 * | |||
totalManhour * | |||
manhourPercentageByGrade[column.id], | |||
0, | |||
@@ -52,7 +52,7 @@ const TaskSetup: React.FC<Props> = ({ | |||
(e: SelectChangeEvent<number | "All">) => { | |||
if (e.target.value === "All" || isNumber(e.target.value)) { | |||
setSelectedTaskTemplateId(e.target.value); | |||
onReset(); | |||
// onReset(); | |||
} | |||
}, | |||
[onReset], | |||