From ccbb8942bc56b3bede88ec2a1192b99fa75a6853 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 8 May 2024 14:21:01 +0800 Subject: [PATCH] update edit project --- .../CreateProject/CreateProject.tsx | 80 +++++++++++++++---- .../CreateProject/ProjectTotalFee.tsx | 18 ++++- .../CreateProject/ResourceAllocation.tsx | 63 ++++++++++++--- .../NavigationContent/NavigationContent.tsx | 2 +- .../TableCellEdit/TableCellEdit.tsx | 11 ++- 5 files changed, 141 insertions(+), 33 deletions(-) diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 5511464..4166be0 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -78,6 +78,14 @@ const hasErrorsInTab = ( return ( errors.projectName || errors.projectCode || errors.projectDescription ); + case 2: + return ( + errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups + ); + case 3: + return ( + errors.milestones + ) default: false; } @@ -132,7 +140,31 @@ const CreateProject: React.FC = ({ const onSubmit = useCallback>( async (data, event) => { try { - console.log("first"); + console.log(data); + + // detect errors + let hasErrors = false + if (data.totalManhour === null || data.totalManhour <= 0) { + formProps.setError("totalManhour", { message: "totalManhour value is not valid", type: "required" }) + 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) { + formProps.setError("manhourPercentageByGrade", { message: "manhourPercentageByGrade value is not valid", type: "invalid" }) + 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"}) + hasErrors = true + } + + if (hasErrors) return false + // save project setServerError(""); let title = t("Do you want to submit?"); @@ -185,6 +217,7 @@ const CreateProject: React.FC = ({ const onSubmitError = useCallback>( (errors) => { + console.log(errors) // Set the tab so that the focus will go there if ( errors.projectName || @@ -192,6 +225,10 @@ const CreateProject: React.FC = ({ errors.projectCode ) { setTabIndex(0); + } else if (errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups) { + setTabIndex(2) + } else if (errors.milestones) { + setTabIndex(3) } }, [], @@ -208,8 +245,8 @@ 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]: 1 / grades.length }; + }, {}) : defaultInputs?.manhourPercentageByGrade, }, }); @@ -253,15 +290,15 @@ const CreateProject: React.FC = ({ formProps.getValues("projectActualStart") && formProps.getValues("projectActualEnd") ) && ( - - )} + + )} )} = ({ > @@ -278,12 +316,26 @@ const CreateProject: React.FC = ({ } iconPosition="end" /> - + + ) : undefined + } iconPosition="end" /> - + ) + : undefined} + iconPosition="end" /> { = ({ taskGroups }) => { const { t } = useTranslation(); - const { watch } = useFormContext(); + const { watch, setError, clearErrors } = 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) => { const payments = milestones[group.id]?.payments || []; const paymentTotal = payments.reduce((acc, p) => acc + p.amount, 0); + projectTotal += paymentTotal; return ( @@ -41,9 +51,9 @@ const ProjectTotalFee: React.FC = ({ taskGroups }) => { {t("Project Total Fee")} {moneyFormatter.format(projectTotal)} - {projectTotal > expectedTotalFee && ( + {projectTotal !== expectedTotalFee && ( - {t("Project total fee is larger than the expected total fee!")} + {t("Project total fee should be same as the expected total fee!")} )} diff --git a/src/components/CreateProject/ResourceAllocation.tsx b/src/components/CreateProject/ResourceAllocation.tsx index c593580..c1e2706 100644 --- a/src/components/CreateProject/ResourceAllocation.tsx +++ b/src/components/CreateProject/ResourceAllocation.tsx @@ -45,9 +45,20 @@ const leftRightBorderCellSx: SxProps = { borderColor: "divider", }; +const errorCellSx: SxProps = { + outline: "1px solid", + outlineColor: "error.main", + + // borderLeft: "1px solid", + // borderRight: "1px solid", + // borderTop: "1px solid", + // borderBottom: "1px solid", + // borderColor: 'error.main' +} + const ResourceAllocationByGrade: React.FC = ({ grades }) => { const { t } = useTranslation(); - const { watch, register, setValue } = useFormContext(); + const { watch, register, setValue, formState: { errors }, setError, clearErrors } = useFormContext(); const manhourPercentageByGrade = watch("manhourPercentageByGrade"); const totalManhour = watch("totalManhour"); @@ -59,10 +70,20 @@ const ResourceAllocationByGrade: React.FC = ({ grades }) => { const makeUpdatePercentage = useCallback( (gradeId: Grade["id"]) => (percentage?: number) => { if (percentage !== undefined) { - setValue("manhourPercentageByGrade", { + 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) !== 1) { + setError("manhourPercentageByGrade", {message: "manhourPercentageByGrade value is not valid", type: "invalid"}) + } else { + clearErrors("manhourPercentageByGrade") + } + } }, [manhourPercentageByGrade, setValue], @@ -79,7 +100,10 @@ const ResourceAllocationByGrade: React.FC = ({ grades }) => { type="number" {...register("totalManhour", { valueAsNumber: true, + required: "totalManhour code required!", + min: 1, })} + error={Boolean(errors.totalManhour)} /> ({ @@ -115,9 +139,10 @@ const ResourceAllocationByGrade: React.FC = ({ grades }) => { convertValue={(inputValue) => Number(inputValue)} cellSx={{ backgroundColor: "primary.lightest" }} inputSx={{ width: "3rem" }} + error={manhourPercentageByGrade[column.id] < 0} /> ))} - + {percentFormatter.format(totalPercentage)} @@ -144,7 +169,7 @@ const ResourceAllocationByGrade: React.FC = ({ grades }) => { const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { const { t } = useTranslation(); - const { watch, setValue } = useFormContext(); + const { watch, setValue, clearErrors, setError } = useFormContext(); const currentTaskGroups = watch("taskGroups"); const taskGroups = useMemo( @@ -167,13 +192,22 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { const makeUpdatePercentage = useCallback( (taskGroupId: TaskGroup["id"]) => (percentage?: number) => { if (percentage !== undefined) { - setValue("taskGroups", { + const updatedTaskGroups = { ...currentTaskGroups, [taskGroupId]: { ...currentTaskGroups[taskGroupId], percentAllocation: percentage, }, - }); + } + 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) !== 1) { + setError("taskGroups", {message: "Task Groups value is not invalid", type: "invalid"}) + } else { + clearErrors("taskGroups") + } } }, [currentTaskGroups, setValue], @@ -219,8 +253,11 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { renderValue={(val) => percentFormatter.format(val)} onChange={makeUpdatePercentage(tg.id)} convertValue={(inputValue) => Number(inputValue)} - cellSx={{ backgroundColor: "primary.lightest" }} + cellSx={{ + backgroundColor: "primary.lightest", + }} inputSx={{ width: "3rem" }} + error={currentTaskGroups[tg.id].percentAllocation < 0} /> {manhourFormatter.format( @@ -248,7 +285,11 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { 0, )} - + acc + tg.percentAllocation, 0,) === 1 && leftBorderCellSx), + ...(Object.values(currentTaskGroups).reduce((acc, tg) => acc + tg.percentAllocation, 0,) !== 1 && errorCellSx) + }} + > {percentFormatter.format( Object.values(currentTaskGroups).reduce( (acc, tg) => acc + tg.percentAllocation, @@ -269,8 +310,8 @@ const ResourceAllocationByStage: React.FC = ({ grades, allTasks }) => { (acc, tg) => acc + tg.percentAllocation * - totalManhour * - manhourPercentageByGrade[column.id], + totalManhour * + manhourPercentageByGrade[column.id], 0, ); return ( diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 226482e..36beaed 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -127,7 +127,7 @@ const NavigationContent: React.FC = ({ abilities }) => { {icon: , label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"}, {icon: , label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, {icon: , label:"Financial Status Report", path: "/analytics/FinancialStatusReport"}, - {icon: , label:"EX02 - Project Cash Flow Report", path: "/analytics/EX02ProjectCashFlowReport"}, + {icon: , label:"Project Cash Flow Report", path: "/analytics/ProjectCashFlowReport"}, ], }, { diff --git a/src/components/TableCellEdit/TableCellEdit.tsx b/src/components/TableCellEdit/TableCellEdit.tsx index 0a366fc..6d88704 100644 --- a/src/components/TableCellEdit/TableCellEdit.tsx +++ b/src/components/TableCellEdit/TableCellEdit.tsx @@ -6,6 +6,7 @@ import React, { useState, } from "react"; import { Box, Input, SxProps, TableCell } from "@mui/material"; +import palette from "@/theme/devias-material-kit/palette"; interface Props { value: T; @@ -14,6 +15,7 @@ interface Props { convertValue: (inputValue: string) => T; cellSx?: SxProps; inputSx?: SxProps; + error?: Boolean; } const TableCellEdit = ({ @@ -23,8 +25,10 @@ const TableCellEdit = ({ onChange, cellSx, inputSx, + error, }: Props) => { const [editMode, setEditMode] = useState(false); + // const [afterEdit, setAfterEdit] = useState(false); const [input, setInput] = useState(""); const inputRef = useRef(null); @@ -40,6 +44,7 @@ const TableCellEdit = ({ const onBlur = useCallback(() => { setEditMode(false); + // setAfterEdit(true) onChange(convertValue(input)); setInput(""); }, [convertValue, input, onChange]); @@ -53,8 +58,8 @@ const TableCellEdit = ({ return ( @@ -76,7 +81,7 @@ const TableCellEdit = ({ onBlur={onBlur} type={typeof value === "number" ? "number" : "text"} /> - + {renderValue(value)}