Quellcode durchsuchen

update edit project

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui vor 1 Jahr
Ursprung
Commit
ccbb8942bc
5 geänderte Dateien mit 141 neuen und 33 gelöschten Zeilen
  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 Datei anzeigen

@@ -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<Props> = ({
const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>(
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<Props> = ({

const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>(
(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<Props> = ({
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<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]: 1 / grades.length };
}, {})
: defaultInputs?.manhourPercentageByGrade,
},
});
@@ -253,15 +290,15 @@ const CreateProject: React.FC<Props> = ({
formProps.getValues("projectActualStart") &&
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>
)}
<Tabs
@@ -271,6 +308,7 @@ const CreateProject: React.FC<Props> = ({
>
<Tab
label={t("Project and Client Details")}
sx={{ marginInlineEnd: !hasErrorsInTab(1, errors) && (hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors)) ? 1 : undefined }}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
@@ -278,12 +316,26 @@ const CreateProject: React.FC<Props> = ({
}
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
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"
/>
<Tab label={t("Milestone")} iconPosition="end" />
<Tab label={t("Milestone")}
icon={
hasErrorsInTab(3, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />)
: undefined}
iconPosition="end" />
</Tabs>
{
<ProjectClientDetails


+ 14
- 4
src/components/CreateProject/ProjectTotalFee.tsx Datei anzeigen

@@ -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 from "react";
import React, { useCallback, useEffect, useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";

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

const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => {
const { t } = useTranslation();
const { watch } = useFormContext<CreateProjectInputs>();
const { watch, setError, clearErrors } = 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) => {
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<Props> = ({ taskGroups }) => {
<Typography variant="h6">{t("Project Total Fee")}</Typography>
<Typography>{moneyFormatter.format(projectTotal)}</Typography>
</Stack>
{projectTotal > expectedTotalFee && (
{projectTotal !== expectedTotalFee && (
<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>
)}
</Stack>


+ 52
- 11
src/components/CreateProject/ResourceAllocation.tsx Datei anzeigen

@@ -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<Props> = ({ grades }) => {
const { t } = useTranslation();
const { watch, register, setValue } = useFormContext<CreateProjectInputs>();
const { watch, register, setValue, formState: { errors }, setError, clearErrors } = useFormContext<CreateProjectInputs>();

const manhourPercentageByGrade = watch("manhourPercentageByGrade");
const totalManhour = watch("totalManhour");
@@ -59,10 +70,20 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ 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<Props> = ({ grades }) => {
type="number"
{...register("totalManhour", {
valueAsNumber: true,
required: "totalManhour code required!",
min: 1,
})}
error={Boolean(errors.totalManhour)}
/>
<Box
sx={(theme) => ({
@@ -115,9 +139,10 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {
convertValue={(inputValue) => Number(inputValue)}
cellSx={{ backgroundColor: "primary.lightest" }}
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)}
</TableCell>
</TableRow>
@@ -144,7 +169,7 @@ const ResourceAllocationByGrade: React.FC<Props> = ({ grades }) => {

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

const currentTaskGroups = watch("taskGroups");
const taskGroups = useMemo(
@@ -167,13 +192,22 @@ const ResourceAllocationByStage: React.FC<Props> = ({ 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<Props> = ({ 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}
/>
<TableCell sx={rightBorderCellSx}>
{manhourFormatter.format(
@@ -248,7 +285,11 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
0,
)}
</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(
Object.values(currentTaskGroups).reduce(
(acc, tg) => acc + tg.percentAllocation,
@@ -269,8 +310,8 @@ const ResourceAllocationByStage: React.FC<Props> = ({ grades, allTasks }) => {
(acc, tg) =>
acc +
tg.percentAllocation *
totalManhour *
manhourPercentageByGrade[column.id],
totalManhour *
manhourPercentageByGrade[column.id],
0,
);
return (


+ 1
- 1
src/components/NavigationContent/NavigationContent.tsx Datei anzeigen

@@ -127,7 +127,7 @@ const NavigationContent: React.FC<Props> = ({ abilities }) => {
{icon: <Analytics />, label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"},
{icon: <Analytics />, label:"Project P&L Report", path: "/analytics/ProjectPLReport"},
{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 Datei anzeigen

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

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

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

const onBlur = useCallback(() => {
setEditMode(false);
// setAfterEdit(true)
onChange(convertValue(input));
setInput("");
}, [convertValue, input, onChange]);
@@ -53,8 +58,8 @@ const TableCellEdit = <T,>({
return (
<TableCell
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,
}}
>
@@ -76,7 +81,7 @@ const TableCellEdit = <T,>({
onBlur={onBlur}
type={typeof value === "number" ? "number" : "text"}
/>
<Box sx={{ display: editMode ? "none" : "block" }} onClick={onClick}>
<Box sx={{ display: editMode ? "none" : "block" }} onClick={onClick}>
{renderValue(value)}
</Box>
</TableCell>


Laden…
Abbrechen
Speichern