From 2c8dc63bce9d25abbf16fac0a01fde9be1d55e78 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 9 May 2024 11:35:04 +0800 Subject: [PATCH] update project --- .../CreateProject/CreateProject.tsx | 34 ++++++++++++-- src/components/CreateProject/Milestone.tsx | 33 ++++++++++++- .../CreateProject/MilestoneSection.tsx | 46 ++++++++++++------- .../CreateProject/ProjectTotalFee.tsx | 13 +----- .../CreateProject/ResourceAllocation.tsx | 35 +++++++------- src/components/CreateProject/TaskSetup.tsx | 2 +- 6 files changed, 112 insertions(+), 51 deletions(-) diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 4166be0..a8a13a8 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -144,22 +144,48 @@ const CreateProject: React.FC = ({ // 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 = ({ // 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, }, diff --git a/src/components/CreateProject/Milestone.tsx b/src/components/CreateProject/Milestone.tsx index d59990d..66dcb2d 100644 --- a/src/components/CreateProject/Milestone.tsx +++ b/src/components/CreateProject/Milestone.tsx @@ -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 = ({ allTasks, isActive }) => { const { t } = useTranslation(); - const { watch } = useFormContext(); + const { watch, setError, clearErrors } = useFormContext(); const currentTaskGroups = watch("taskGroups"); const taskGroups = useMemo( () => @@ -57,6 +57,35 @@ const Milestone: React.FC = ({ 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 ( <> diff --git a/src/components/CreateProject/MilestoneSection.tsx b/src/components/CreateProject/MilestoneSection.tsx index 9984875..5d66480 100644 --- a/src/components/CreateProject/MilestoneSection.tsx +++ b/src/components/CreateProject/MilestoneSection.tsx @@ -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 = ({ 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 = ({ taskGroupId }) => { - { - if (!date) return; - const milestones = getValues("milestones"); - setValue("milestones", { - ...milestones, - [taskGroupId]: { - ...milestones[taskGroupId], - startDate: date.format(INPUT_DATE_FORMAT), - }, - }); - }} - /> + { + 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)), + }, + }} + /> @@ -272,6 +279,11 @@ const MilestoneSection: React.FC = ({ taskGroupId }) => { }, }); }} + slotProps={{ + textField: { + error: endDate === "Invalid Date" || new Date(startDate) > new Date(endDate) || (Boolean(formState.errors.milestones) && !Boolean(endDate)), + }, + }} /> diff --git a/src/components/CreateProject/ProjectTotalFee.tsx b/src/components/CreateProject/ProjectTotalFee.tsx index 247ed2d..edc7a71 100644 --- a/src/components/CreateProject/ProjectTotalFee.tsx +++ b/src/components/CreateProject/ProjectTotalFee.tsx @@ -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 = ({ taskGroups }) => { const { t } = useTranslation(); - const { watch, setError, clearErrors } = useFormContext(); + const { watch } = useFormContext(); 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 ( {taskGroups.map((group, index) => { diff --git a/src/components/CreateProject/ResourceAllocation.tsx b/src/components/CreateProject/ResourceAllocation.tsx index c1e2706..46cec67 100644 --- a/src/components/CreateProject/ResourceAllocation.tsx +++ b/src/components/CreateProject/ResourceAllocation.tsx @@ -62,10 +62,10 @@ const ResourceAllocationByGrade: React.FC = ({ 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 = ({ 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 = ({ grades }) => { 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 = ({ grades }) => { error={manhourPercentageByGrade[column.id] < 0} /> ))} - - {percentFormatter.format(totalPercentage)} + + {totalPercentage + "%"} + {/* {percentFormatter.format(totalPercentage)} */} @@ -151,7 +153,7 @@ const ResourceAllocationByGrade: React.FC = ({ grades }) => { {grades.map((column, idx) => ( {manhourFormatter.format( - manhourPercentageByGrade[column.id] * totalManhour, + manhourPercentageByGrade[column.id] / 100 * totalManhour, )} ))} @@ -203,7 +205,7 @@ const ResourceAllocationByStage: React.FC = ({ 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 = ({ grades, allTasks }) => { 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 = ({ grades, allTasks }) => { /> {manhourFormatter.format( - currentTaskGroups[tg.id].percentAllocation * totalManhour, + currentTaskGroups[tg.id].percentAllocation / 100 * totalManhour, )} {grades.map((column, idx) => { @@ -270,7 +273,7 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { return ( {manhourFormatter.format( - manhourPercentageByGrade[column.id] * stageHours, + manhourPercentageByGrade[column.id] / 100 * stageHours, )} ); @@ -286,13 +289,13 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { )} 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 = ({ grades, allTasks }) => { {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 = ({ grades, allTasks }) => { const hours = Object.values(currentTaskGroups).reduce( (acc, tg) => acc + - tg.percentAllocation * + tg.percentAllocation / 100 * totalManhour * manhourPercentageByGrade[column.id], 0, diff --git a/src/components/CreateProject/TaskSetup.tsx b/src/components/CreateProject/TaskSetup.tsx index 443014e..6651971 100644 --- a/src/components/CreateProject/TaskSetup.tsx +++ b/src/components/CreateProject/TaskSetup.tsx @@ -52,7 +52,7 @@ const TaskSetup: React.FC = ({ (e: SelectChangeEvent) => { if (e.target.value === "All" || isNumber(e.target.value)) { setSelectedTaskTemplateId(e.target.value); - onReset(); + // onReset(); } }, [onReset],