浏览代码

update project

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui 1年前
父节点
当前提交
2c8dc63bce
共有 6 个文件被更改,包括 112 次插入51 次删除
  1. +30
    -4
      src/components/CreateProject/CreateProject.tsx
  2. +31
    -2
      src/components/CreateProject/Milestone.tsx
  3. +29
    -17
      src/components/CreateProject/MilestoneSection.tsx
  4. +2
    -11
      src/components/CreateProject/ProjectTotalFee.tsx
  5. +19
    -16
      src/components/CreateProject/ResourceAllocation.tsx
  6. +1
    -1
      src/components/CreateProject/TaskSetup.tsx

+ 30
- 4
src/components/CreateProject/CreateProject.tsx 查看文件

@@ -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,
},


+ 31
- 2
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<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" }}>


+ 29
- 17
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<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
- 11
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<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) => {


+ 19
- 16
src/components/CreateProject/ResourceAllocation.tsx 查看文件

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


+ 1
- 1
src/components/CreateProject/TaskSetup.tsx 查看文件

@@ -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],


正在加载...
取消
保存