浏览代码

update edit project

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui 1年前
父节点
当前提交
ccbb8942bc
共有 5 个文件被更改,包括 141 次插入33 次删除
  1. +66
    -14
      src/components/CreateProject/CreateProject.tsx
  2. +14
    -4
      src/components/CreateProject/ProjectTotalFee.tsx
  3. +52
    -11
      src/components/CreateProject/ResourceAllocation.tsx
  4. +1
    -1
      src/components/NavigationContent/NavigationContent.tsx
  5. +8
    -3
      src/components/TableCellEdit/TableCellEdit.tsx

+ 66
- 14
src/components/CreateProject/CreateProject.tsx 查看文件

@@ -78,6 +78,14 @@ const hasErrorsInTab = (
return ( return (
errors.projectName || errors.projectCode || errors.projectDescription errors.projectName || errors.projectCode || errors.projectDescription
); );
case 2:
return (
errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups
);
case 3:
return (
errors.milestones
)
default: default:
false; false;
} }
@@ -132,7 +140,31 @@ const CreateProject: React.FC<Props> = ({
const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>( const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>(
async (data, event) => { async (data, event) => {
try { 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(""); setServerError("");


let title = t("Do you want to submit?"); let title = t("Do you want to submit?");
@@ -185,6 +217,7 @@ const CreateProject: React.FC<Props> = ({


const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>(
(errors) => { (errors) => {
console.log(errors)
// Set the tab so that the focus will go there // Set the tab so that the focus will go there
if ( if (
errors.projectName || errors.projectName ||
@@ -192,6 +225,10 @@ const CreateProject: React.FC<Props> = ({
errors.projectCode errors.projectCode
) { ) {
setTabIndex(0); 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<Props> = ({
// manhourPercentageByGrade should have a sensible default // manhourPercentageByGrade should have a sensible default
manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade) manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade)
? grades.reduce((acc, grade) => { ? grades.reduce((acc, grade) => {
return { ...acc, [grade.id]: 1 / grades.length };
}, {})
return { ...acc, [grade.id]: 1 / grades.length };
}, {})
: defaultInputs?.manhourPercentageByGrade, : defaultInputs?.manhourPercentageByGrade,
}, },
}); });
@@ -253,15 +290,15 @@ const CreateProject: React.FC<Props> = ({
formProps.getValues("projectActualStart") && formProps.getValues("projectActualStart") &&
formProps.getValues("projectActualEnd") formProps.getValues("projectActualEnd")
) && ( ) && (
<Button
variant="outlined"
startIcon={<Delete />}
color="error"
onClick={handleDelete}
>
{t("Delete Project")}
</Button>
)}
<Button
variant="outlined"
startIcon={<Delete />}
color="error"
onClick={handleDelete}
>
{t("Delete Project")}
</Button>
)}
</Stack> </Stack>
)} )}
<Tabs <Tabs
@@ -271,6 +308,7 @@ const CreateProject: React.FC<Props> = ({
> >
<Tab <Tab
label={t("Project and Client Details")} label={t("Project and Client Details")}
sx={{ marginInlineEnd: !hasErrorsInTab(1, errors) && (hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors)) ? 1 : undefined }}
icon={ icon={
hasErrorsInTab(0, errors) ? ( hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" /> <Error sx={{ marginInlineEnd: 1 }} color="error" />
@@ -278,12 +316,26 @@ const CreateProject: React.FC<Props> = ({
} }
iconPosition="end" iconPosition="end"
/> />
<Tab label={t("Project Task Setup")} iconPosition="end" />
<Tab
label={t("Project Task Setup")}
sx={{ marginInlineEnd: hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors) ? 1 : undefined }}
iconPosition="end" />
<Tab <Tab
label={t("Staff Allocation and Resource")} label={t("Staff Allocation and Resource")}
sx={{ marginInlineEnd: !hasErrorsInTab(2, errors) && hasErrorsInTab(3, errors) ? 1 : undefined }}
icon={
hasErrorsInTab(2, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end" iconPosition="end"
/> />
<Tab label={t("Milestone")} iconPosition="end" />
<Tab label={t("Milestone")}
icon={
hasErrorsInTab(3, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />)
: undefined}
iconPosition="end" />
</Tabs> </Tabs>
{ {
<ProjectClientDetails <ProjectClientDetails


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

@@ -2,7 +2,7 @@ import { CreateProjectInputs } from "@/app/api/projects/actions";
import { TaskGroup } from "@/app/api/tasks"; import { TaskGroup } from "@/app/api/tasks";
import { moneyFormatter } from "@/app/utils/formatUtil"; import { moneyFormatter } from "@/app/utils/formatUtil";
import { Divider, Stack, Typography } from "@mui/material"; import { Divider, Stack, Typography } from "@mui/material";
import React from "react";
import React, { useCallback, useEffect, useMemo } from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";


@@ -12,17 +12,27 @@ interface Props {


const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => { const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { watch } = useFormContext<CreateProjectInputs>();
const { watch, setError, clearErrors } = useFormContext<CreateProjectInputs>();
const milestones = watch("milestones"); const milestones = watch("milestones");
const expectedTotalFee = watch("expectedProjectFee"); const expectedTotalFee = watch("expectedProjectFee");


let projectTotal = 0; 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 ( return (
<Stack spacing={1}> <Stack spacing={1}>
{taskGroups.map((group, index) => { {taskGroups.map((group, index) => {
const payments = milestones[group.id]?.payments || []; const payments = milestones[group.id]?.payments || [];
const paymentTotal = payments.reduce((acc, p) => acc + p.amount, 0); const paymentTotal = payments.reduce((acc, p) => acc + p.amount, 0);

projectTotal += paymentTotal; projectTotal += paymentTotal;


return ( return (
@@ -41,9 +51,9 @@ const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => {
<Typography variant="h6">{t("Project Total Fee")}</Typography> <Typography variant="h6">{t("Project Total Fee")}</Typography>
<Typography>{moneyFormatter.format(projectTotal)}</Typography> <Typography>{moneyFormatter.format(projectTotal)}</Typography>
</Stack> </Stack>
{projectTotal > expectedTotalFee && (
{projectTotal !== expectedTotalFee && (
<Typography variant="caption" color="warning.main" alignSelf="flex-end"> <Typography variant="caption" color="warning.main" alignSelf="flex-end">
{t("Project total fee is larger than the expected total fee!")}
{t("Project total fee should be same as the expected total fee!")}
</Typography> </Typography>
)} )}
</Stack> </Stack>


+ 52
- 11
src/components/CreateProject/ResourceAllocation.tsx 查看文件

@@ -45,9 +45,20 @@ const leftRightBorderCellSx: SxProps = {
borderColor: "divider", 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<Props> = ({ grades }) => { const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { watch, register, setValue } = useFormContext<CreateProjectInputs>();
const { watch, register, setValue, formState: { errors }, setError, clearErrors } = useFormContext<CreateProjectInputs>();


const manhourPercentageByGrade = watch("manhourPercentageByGrade"); const manhourPercentageByGrade = watch("manhourPercentageByGrade");
const totalManhour = watch("totalManhour"); const totalManhour = watch("totalManhour");
@@ -59,10 +70,20 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {
const makeUpdatePercentage = useCallback( const makeUpdatePercentage = useCallback(
(gradeId: Grade["id"]) => (percentage?: number) => { (gradeId: Grade["id"]) => (percentage?: number) => {
if (percentage !== undefined) { if (percentage !== undefined) {
setValue("manhourPercentageByGrade", {
const updatedManhourPercentageByGrade = {
...manhourPercentageByGrade, ...manhourPercentageByGrade,
[gradeId]: percentage, [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], [manhourPercentageByGrade, setValue],
@@ -79,7 +100,10 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {
type="number" type="number"
{...register("totalManhour", { {...register("totalManhour", {
valueAsNumber: true, valueAsNumber: true,
required: "totalManhour code required!",
min: 1,
})} })}
error={Boolean(errors.totalManhour)}
/> />
<Box <Box
sx={(theme) => ({ sx={(theme) => ({
@@ -115,9 +139,10 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {
convertValue={(inputValue) => Number(inputValue)} convertValue={(inputValue) => Number(inputValue)}
cellSx={{ backgroundColor: "primary.lightest" }} cellSx={{ backgroundColor: "primary.lightest" }}
inputSx={{ width: "3rem" }} inputSx={{ width: "3rem" }}
error={manhourPercentageByGrade[column.id] < 0}
/> />
))} ))}
<TableCell sx={leftBorderCellSx}>
<TableCell sx={{ ...(totalPercentage === 1 && leftBorderCellSx), ...(totalPercentage !== 1 && {...errorCellSx, borderRight: "1px solid", borderColor: "error.main"})}}>
{percentFormatter.format(totalPercentage)} {percentFormatter.format(totalPercentage)}
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -144,7 +169,7 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {


const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => { const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { watch, setValue } = useFormContext<CreateProjectInputs>();
const { watch, setValue, clearErrors, setError } = useFormContext<CreateProjectInputs>();


const currentTaskGroups = watch("taskGroups"); const currentTaskGroups = watch("taskGroups");
const taskGroups = useMemo( const taskGroups = useMemo(
@@ -167,13 +192,22 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
const makeUpdatePercentage = useCallback( const makeUpdatePercentage = useCallback(
(taskGroupId: TaskGroup["id"]) => (percentage?: number) => { (taskGroupId: TaskGroup["id"]) => (percentage?: number) => {
if (percentage !== undefined) { if (percentage !== undefined) {
setValue("taskGroups", {
const updatedTaskGroups = {
...currentTaskGroups, ...currentTaskGroups,
[taskGroupId]: { [taskGroupId]: {
...currentTaskGroups[taskGroupId], ...currentTaskGroups[taskGroupId],
percentAllocation: percentage, 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], [currentTaskGroups, setValue],
@@ -219,8 +253,11 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
renderValue={(val) => percentFormatter.format(val)} renderValue={(val) => percentFormatter.format(val)}
onChange={makeUpdatePercentage(tg.id)} onChange={makeUpdatePercentage(tg.id)}
convertValue={(inputValue) => Number(inputValue)} convertValue={(inputValue) => Number(inputValue)}
cellSx={{ backgroundColor: "primary.lightest" }}
cellSx={{
backgroundColor: "primary.lightest",
}}
inputSx={{ width: "3rem" }} inputSx={{ width: "3rem" }}
error={currentTaskGroups[tg.id].percentAllocation < 0}
/> />
<TableCell sx={rightBorderCellSx}> <TableCell sx={rightBorderCellSx}>
{manhourFormatter.format( {manhourFormatter.format(
@@ -248,7 +285,11 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
0, 0,
)} )}
</TableCell> </TableCell>
<TableCell sx={leftBorderCellSx}>
<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)
}}
>
{percentFormatter.format( {percentFormatter.format(
Object.values(currentTaskGroups).reduce( Object.values(currentTaskGroups).reduce(
(acc, tg) => acc + tg.percentAllocation, (acc, tg) => acc + tg.percentAllocation,
@@ -269,8 +310,8 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
(acc, tg) => (acc, tg) =>
acc + acc +
tg.percentAllocation * tg.percentAllocation *
totalManhour *
manhourPercentageByGrade[column.id],
totalManhour *
manhourPercentageByGrade[column.id],
0, 0,
); );
return ( return (


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

@@ -127,7 +127,7 @@ const NavigationContent: React.FC<Props> = ({ abilities }) => {
{icon: <Analytics />, label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"}, {icon: <Analytics />, label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"},
{icon: <Analytics />, label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, {icon: <Analytics />, label:"Project P&L Report", path: "/analytics/ProjectPLReport"},
{icon: <Analytics />, label:"Financial Status Report", path: "/analytics/FinancialStatusReport"}, {icon: <Analytics />, label:"Financial Status Report", path: "/analytics/FinancialStatusReport"},
{icon: <Analytics />, label:"EX02 - Project Cash Flow Report", path: "/analytics/EX02ProjectCashFlowReport"},
{icon: <Analytics />, label:"Project Cash Flow Report", path: "/analytics/ProjectCashFlowReport"},
], ],
}, },
{ {


+ 8
- 3
src/components/TableCellEdit/TableCellEdit.tsx 查看文件

@@ -6,6 +6,7 @@ import React, {
useState, useState,
} from "react"; } from "react";
import { Box, Input, SxProps, TableCell } from "@mui/material"; import { Box, Input, SxProps, TableCell } from "@mui/material";
import palette from "@/theme/devias-material-kit/palette";


interface Props<T> { interface Props<T> {
value: T; value: T;
@@ -14,6 +15,7 @@ interface Props<T> {
convertValue: (inputValue: string) => T; convertValue: (inputValue: string) => T;
cellSx?: SxProps; cellSx?: SxProps;
inputSx?: SxProps; inputSx?: SxProps;
error?: Boolean;
} }


const TableCellEdit = <T,>({ const TableCellEdit = <T,>({
@@ -23,8 +25,10 @@ const TableCellEdit = <T,>({
onChange, onChange,
cellSx, cellSx,
inputSx, inputSx,
error,
}: Props<T>) => { }: Props<T>) => {
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
// const [afterEdit, setAfterEdit] = useState(false);
const [input, setInput] = useState<string>(""); const [input, setInput] = useState<string>("");
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);


@@ -40,6 +44,7 @@ const TableCellEdit = <T,>({


const onBlur = useCallback(() => { const onBlur = useCallback(() => {
setEditMode(false); setEditMode(false);
// setAfterEdit(true)
onChange(convertValue(input)); onChange(convertValue(input));
setInput(""); setInput("");
}, [convertValue, input, onChange]); }, [convertValue, input, onChange]);
@@ -53,8 +58,8 @@ const TableCellEdit = <T,>({
return ( return (
<TableCell <TableCell
sx={{ sx={{
outline: editMode ? "1px solid" : undefined,
outlineColor: editMode ? "primary.main" : undefined,
outline: editMode && !error ? "1px solid" : error ? "1px solid" : undefined,
outlineColor: editMode && !error ? "primary.main" : error ? "error.main" : undefined,
...cellSx, ...cellSx,
}} }}
> >
@@ -76,7 +81,7 @@ const TableCellEdit = <T,>({
onBlur={onBlur} onBlur={onBlur}
type={typeof value === "number" ? "number" : "text"} type={typeof value === "number" ? "number" : "text"}
/> />
<Box sx={{ display: editMode ? "none" : "block" }} onClick={onClick}>
<Box sx={{ display: editMode ? "none" : "block" }} onClick={onClick}>
{renderValue(value)} {renderValue(value)}
</Box> </Box>
</TableCell> </TableCell>


正在加载...
取消
保存