@@ -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 | ||||
@@ -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> | ||||
@@ -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 ( | ||||
@@ -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"}, | |||||
], | ], | ||||
}, | }, | ||||
{ | { | ||||
@@ -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> | ||||