@@ -1,4 +1,4 @@ | |||||
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | |||||
import { fetchAllCustomers, fetchAllSubsidiaries, fetchCustomerTypes } from "@/app/api/customer"; | |||||
import { fetchGrades } from "@/app/api/grades"; | import { fetchGrades } from "@/app/api/grades"; | ||||
import { | import { | ||||
fetchProjectBuildingTypes, | fetchProjectBuildingTypes, | ||||
@@ -26,9 +26,9 @@ export const metadata: Metadata = { | |||||
const Projects: React.FC = async () => { | const Projects: React.FC = async () => { | ||||
const { t } = await getServerI18n("projects"); | const { t } = await getServerI18n("projects"); | ||||
const abilities = await getUserAbilities() | |||||
const abilities = await getUserAbilities(); | |||||
if (![MAINTAIN_PROJECT].some(ability => abilities.includes(ability))) { | |||||
if (![MAINTAIN_PROJECT].some((ability) => abilities.includes(ability))) { | |||||
notFound(); | notFound(); | ||||
} | } | ||||
@@ -44,6 +44,7 @@ const Projects: React.FC = async () => { | |||||
fetchProjectWorkNatures(); | fetchProjectWorkNatures(); | ||||
fetchAllCustomers(); | fetchAllCustomers(); | ||||
fetchAllSubsidiaries(); | fetchAllSubsidiaries(); | ||||
fetchCustomerTypes(); | |||||
fetchGrades(); | fetchGrades(); | ||||
preloadTeamLeads(); | preloadTeamLeads(); | ||||
preloadStaff(); | preloadStaff(); | ||||
@@ -40,6 +40,7 @@ export interface CreateProjectInputs { | |||||
clientSubsidiaryId?: number | null; | clientSubsidiaryId?: number | null; | ||||
subsidiaryContactId?: number; | subsidiaryContactId?: number; | ||||
isSubsidiaryContact?: boolean; | isSubsidiaryContact?: boolean; | ||||
clientTypeId?: number; | |||||
// Allocation | // Allocation | ||||
totalManhour: number; | totalManhour: number; | ||||
@@ -117,12 +118,12 @@ export const deleteProject = async (id: number) => { | |||||
}; | }; | ||||
export const importProjects = async (data: FormData) => { | export const importProjects = async (data: FormData) => { | ||||
const importProjects = await serverFetchString<String>( | |||||
`${BASE_API_URL}/projects/import`, | |||||
{ | |||||
method: "POST", | |||||
body: data, | |||||
}, | |||||
const importProjects = await serverFetchString<string>( | |||||
`${BASE_API_URL}/projects/import`, | |||||
{ | |||||
method: "POST", | |||||
body: data, | |||||
}, | |||||
); | ); | ||||
return importProjects; | return importProjects; | ||||
@@ -41,7 +41,7 @@ import { | |||||
import { StaffResult } from "@/app/api/staff"; | import { StaffResult } from "@/app/api/staff"; | ||||
import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||
import { Grade } from "@/app/api/grades"; | import { Grade } from "@/app/api/grades"; | ||||
import { Customer, Subsidiary } from "@/app/api/customer"; | |||||
import { Customer, CustomerType, Subsidiary } from "@/app/api/customer"; | |||||
import { isEmpty } from "lodash"; | import { isEmpty } from "lodash"; | ||||
import { | import { | ||||
deleteDialog, | deleteDialog, | ||||
@@ -70,6 +70,7 @@ export interface Props { | |||||
buildingTypes: BuildingType[]; | buildingTypes: BuildingType[]; | ||||
workNatures: WorkNature[]; | workNatures: WorkNature[]; | ||||
allStaffs: StaffResult[]; | allStaffs: StaffResult[]; | ||||
customerTypes: CustomerType[]; | |||||
grades: Grade[]; | grades: Grade[]; | ||||
abilities: string[]; | abilities: string[]; | ||||
} | } | ||||
@@ -81,16 +82,19 @@ const hasErrorsInTab = ( | |||||
switch (tabIndex) { | switch (tabIndex) { | ||||
case 0: | case 0: | ||||
return ( | return ( | ||||
errors.projectName || errors.projectDescription || errors.clientId || errors.projectCode | |||||
errors.projectName || | |||||
errors.projectDescription || | |||||
errors.clientId || | |||||
errors.projectCode | |||||
); | ); | ||||
case 2: | case 2: | ||||
return ( | return ( | ||||
errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups | |||||
errors.totalManhour || | |||||
errors.manhourPercentageByGrade || | |||||
errors.taskGroups | |||||
); | ); | ||||
case 3: | case 3: | ||||
return ( | |||||
errors.milestones | |||||
) | |||||
return errors.milestones; | |||||
default: | default: | ||||
false; | false; | ||||
} | } | ||||
@@ -115,6 +119,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
buildingTypes, | buildingTypes, | ||||
workNatures, | workNatures, | ||||
allStaffs, | allStaffs, | ||||
customerTypes, | |||||
abilities, | abilities, | ||||
}) => { | }) => { | ||||
const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
@@ -151,53 +156,102 @@ const CreateProject: React.FC<Props> = ({ | |||||
console.log(data); | console.log(data); | ||||
// detect errors | // detect errors | ||||
let hasErrors = false | |||||
let hasErrors = false; | |||||
// Tab - Staff Allocation and Resource | // Tab - Staff Allocation and Resource | ||||
if (data.totalManhour === null || data.totalManhour <= 0) { | if (data.totalManhour === null || data.totalManhour <= 0) { | ||||
formProps.setError("totalManhour", { message: "totalManhour value is not valid", type: "required" }) | |||||
setTabIndex(2) | |||||
hasErrors = true | |||||
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) !== 100) { | |||||
formProps.setError("manhourPercentageByGrade", { message: "manhourPercentageByGrade value is not valid", type: "invalid" }) | |||||
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, | |||||
) !== 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) !== 100) { | |||||
formProps.setError("taskGroups", { message: "Task Groups value is not invalid", 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, | |||||
) !== 100 | |||||
) { | |||||
formProps.setError("taskGroups", { | |||||
message: "Task Groups value is not invalid", | |||||
type: "invalid", | |||||
}); | |||||
setTabIndex(2); | |||||
hasErrors = true; | |||||
} | } | ||||
// Tab - Milestone | // Tab - Milestone | ||||
let projectTotal = 0 | |||||
const milestonesKeys = Object.keys(data.milestones).filter(key => taskGroupKeys.includes(key)) | |||||
milestonesKeys.filter(key => Object.keys(data.taskGroups).includes(key)).forEach(key => { | |||||
const { startDate, endDate, payments } = data.milestones[parseFloat(key)] | |||||
let projectTotal = 0; | |||||
const milestonesKeys = Object.keys(data.milestones).filter((key) => | |||||
taskGroupKeys.includes(key), | |||||
); | |||||
milestonesKeys | |||||
.filter((key) => Object.keys(data.taskGroups).includes(key)) | |||||
.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 | |||||
} | |||||
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) | |||||
}) | |||||
projectTotal += payments.reduce( | |||||
(acc, payment) => acc + payment.amount, | |||||
0, | |||||
); | |||||
}); | |||||
if (projectTotal !== data.expectedProjectFee || milestonesKeys.length !== taskGroupKeys.length) { | |||||
formProps.setError("milestones", { message: "milestones is not valid", type: "invalid" }) | |||||
setTabIndex(3) | |||||
hasErrors = true | |||||
if ( | |||||
projectTotal !== data.expectedProjectFee || | |||||
milestonesKeys.length !== taskGroupKeys.length | |||||
) { | |||||
formProps.setError("milestones", { | |||||
message: "milestones is not valid", | |||||
type: "invalid", | |||||
}); | |||||
setTabIndex(3); | |||||
hasErrors = true; | |||||
} | } | ||||
if (hasErrors) return false | |||||
if (hasErrors) return false; | |||||
// save project | // save project | ||||
setServerError(""); | setServerError(""); | ||||
@@ -227,18 +281,29 @@ const CreateProject: React.FC<Props> = ({ | |||||
data.projectActualEnd = dayjs().format("YYYY-MM-DD"); | data.projectActualEnd = dayjs().format("YYYY-MM-DD"); | ||||
} | } | ||||
data.taskTemplateId = data.taskTemplateId === "All" ? undefined : data.taskTemplateId; | |||||
data.taskTemplateId = | |||||
data.taskTemplateId === "All" ? undefined : data.taskTemplateId; | |||||
const response = await saveProject(data); | const response = await saveProject(data); | ||||
if (response.id > 0 && response.message?.toLowerCase() === "success" && response.errorPosition === null) { | |||||
if ( | |||||
response.id > 0 && | |||||
response.message?.toLowerCase() === "success" && | |||||
response.errorPosition === null | |||||
) { | |||||
successDialog(successTitle, t).then(() => { | successDialog(successTitle, t).then(() => { | ||||
router.replace("/projects"); | router.replace("/projects"); | ||||
}); | }); | ||||
} else { | } else { | ||||
errorDialog(response.message ?? errorTitle, t).then(() => { | errorDialog(response.message ?? errorTitle, t).then(() => { | ||||
if (response.errorPosition !== null && response.errorPosition === "projectCode") { | |||||
formProps.setError("projectCode", { message: response.message, type: "invalid" }) | |||||
setTabIndex(0) | |||||
if ( | |||||
response.errorPosition !== null && | |||||
response.errorPosition === "projectCode" | |||||
) { | |||||
formProps.setError("projectCode", { | |||||
message: response.message, | |||||
type: "invalid", | |||||
}); | |||||
setTabIndex(0); | |||||
} | } | ||||
return false; | return false; | ||||
@@ -257,7 +322,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | ||||
(errors) => { | (errors) => { | ||||
console.log(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 || | ||||
@@ -266,10 +331,14 @@ const CreateProject: React.FC<Props> = ({ | |||||
errors.clientId | errors.clientId | ||||
) { | ) { | ||||
setTabIndex(0); | setTabIndex(0); | ||||
} else if (errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups) { | |||||
setTabIndex(2) | |||||
} else if ( | |||||
errors.totalManhour || | |||||
errors.manhourPercentageByGrade || | |||||
errors.taskGroups | |||||
) { | |||||
setTabIndex(2); | |||||
} else if (errors.milestones) { | } else if (errors.milestones) { | ||||
setTabIndex(3) | |||||
setTabIndex(3); | |||||
} | } | ||||
}, | }, | ||||
[], | [], | ||||
@@ -282,18 +351,26 @@ const CreateProject: React.FC<Props> = ({ | |||||
milestones: {}, | milestones: {}, | ||||
totalManhour: 0, | totalManhour: 0, | ||||
taskTemplateId: "All", | taskTemplateId: "All", | ||||
projectName: mainProjects !== undefined ? mainProjects[0].projectName : undefined, | |||||
projectDescription: mainProjects !== undefined ? mainProjects[0].projectDescription : undefined, | |||||
expectedProjectFee: mainProjects !== undefined ? mainProjects[0].expectedProjectFee : undefined, | |||||
subContractFee: mainProjects !== undefined ? mainProjects[0].subContractFee : undefined, | |||||
projectName: | |||||
mainProjects !== undefined ? mainProjects[0].projectName : undefined, | |||||
projectDescription: | |||||
mainProjects !== undefined | |||||
? mainProjects[0].projectDescription | |||||
: undefined, | |||||
expectedProjectFee: | |||||
mainProjects !== undefined | |||||
? mainProjects[0].expectedProjectFee | |||||
: undefined, | |||||
subContractFee: | |||||
mainProjects !== undefined ? mainProjects[0].subContractFee : undefined, | |||||
clientId: allCustomers !== undefined ? allCustomers[0].id : undefined, | clientId: allCustomers !== undefined ? allCustomers[0].id : undefined, | ||||
...defaultInputs, | ...defaultInputs, | ||||
// 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]: 100 / grades.length }; | |||||
}, {}) | |||||
return { ...acc, [grade.id]: 100 / grades.length }; | |||||
}, {}) | |||||
: defaultInputs?.manhourPercentageByGrade, | : defaultInputs?.manhourPercentageByGrade, | ||||
}, | }, | ||||
}); | }); | ||||
@@ -311,7 +388,8 @@ const CreateProject: React.FC<Props> = ({ | |||||
{isEditMode && !(formProps.getValues("projectDeleted") === true) && ( | {isEditMode && !(formProps.getValues("projectDeleted") === true) && ( | ||||
<Stack direction="row" gap={1}> | <Stack direction="row" gap={1}> | ||||
{/* {!formProps.getValues("projectActualStart") && ( */} | {/* {!formProps.getValues("projectActualStart") && ( */} | ||||
{formProps.getValues("projectStatus")?.toLowerCase() === "pending to start" && ( | |||||
{formProps.getValues("projectStatus")?.toLowerCase() === | |||||
"pending to start" && ( | |||||
<Button | <Button | ||||
name="start" | name="start" | ||||
type="submit" | type="submit" | ||||
@@ -324,7 +402,8 @@ const CreateProject: React.FC<Props> = ({ | |||||
)} | )} | ||||
{/* {formProps.getValues("projectActualStart") && | {/* {formProps.getValues("projectActualStart") && | ||||
!formProps.getValues("projectActualEnd") && ( */} | !formProps.getValues("projectActualEnd") && ( */} | ||||
{formProps.getValues("projectStatus")?.toLowerCase() === "on-going" && ( | |||||
{formProps.getValues("projectStatus")?.toLowerCase() === | |||||
"on-going" && ( | |||||
<Button | <Button | ||||
name="complete" | name="complete" | ||||
type="submit" | type="submit" | ||||
@@ -338,9 +417,14 @@ const CreateProject: React.FC<Props> = ({ | |||||
{!( | {!( | ||||
// formProps.getValues("projectActualStart") && | // formProps.getValues("projectActualStart") && | ||||
// formProps.getValues("projectActualEnd") | // formProps.getValues("projectActualEnd") | ||||
formProps.getValues("projectStatus")?.toLowerCase() === "completed" || | |||||
formProps.getValues("projectStatus")?.toLowerCase() === "deleted" | |||||
) && abilities.includes(DELETE_PROJECT) && ( | |||||
( | |||||
formProps.getValues("projectStatus")?.toLowerCase() === | |||||
"completed" || | |||||
formProps.getValues("projectStatus")?.toLowerCase() === | |||||
"deleted" | |||||
) | |||||
) && | |||||
abilities.includes(DELETE_PROJECT) && ( | |||||
<Button | <Button | ||||
variant="outlined" | variant="outlined" | ||||
startIcon={<Delete />} | startIcon={<Delete />} | ||||
@@ -359,7 +443,13 @@ 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 }} | |||||
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" /> | ||||
@@ -369,11 +459,22 @@ const CreateProject: React.FC<Props> = ({ | |||||
/> | /> | ||||
<Tab | <Tab | ||||
label={t("Project Task Setup")} | label={t("Project Task Setup")} | ||||
sx={{ marginInlineEnd: hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors) ? 1 : undefined }} | |||||
iconPosition="end" /> | |||||
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 }} | |||||
sx={{ | |||||
marginInlineEnd: | |||||
!hasErrorsInTab(2, errors) && hasErrorsInTab(3, errors) | |||||
? 1 | |||||
: undefined, | |||||
}} | |||||
icon={ | icon={ | ||||
hasErrorsInTab(2, errors) ? ( | hasErrorsInTab(2, errors) ? ( | ||||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | <Error sx={{ marginInlineEnd: 1 }} color="error" /> | ||||
@@ -381,12 +482,15 @@ const CreateProject: React.FC<Props> = ({ | |||||
} | } | ||||
iconPosition="end" | iconPosition="end" | ||||
/> | /> | ||||
<Tab label={t("Milestone")} | |||||
<Tab | |||||
label={t("Milestone")} | |||||
icon={ | icon={ | ||||
hasErrorsInTab(3, errors) ? ( | hasErrorsInTab(3, errors) ? ( | ||||
<Error sx={{ marginInlineEnd: 1 }} color="error" />) | |||||
: undefined} | |||||
iconPosition="end" /> | |||||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||||
) : undefined | |||||
} | |||||
iconPosition="end" | |||||
/> | |||||
</Tabs> | </Tabs> | ||||
{ | { | ||||
<ProjectClientDetails | <ProjectClientDetails | ||||
@@ -401,6 +505,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
allCustomers={allCustomers} | allCustomers={allCustomers} | ||||
allSubsidiaries={allSubsidiaries} | allSubsidiaries={allSubsidiaries} | ||||
projectCategories={projectCategories} | projectCategories={projectCategories} | ||||
customerTypes={customerTypes} | |||||
teamLeads={teamLeads} | teamLeads={teamLeads} | ||||
isActive={tabIndex === 0} | isActive={tabIndex === 0} | ||||
isEditMode={isEditMode} | isEditMode={isEditMode} | ||||
@@ -440,11 +545,14 @@ const CreateProject: React.FC<Props> = ({ | |||||
startIcon={<Check />} | startIcon={<Check />} | ||||
type="submit" | type="submit" | ||||
disabled={ | disabled={ | ||||
formProps.getValues("projectDeleted") === true || formProps.getValues("projectStatus")?.toLowerCase() === "deleted" || | |||||
( | |||||
// !!formProps.getValues("projectActualStart") && | |||||
!!(formProps.getValues("projectStatus")?.toLowerCase() === "completed") | |||||
) | |||||
formProps.getValues("projectDeleted") === true || | |||||
formProps.getValues("projectStatus")?.toLowerCase() === | |||||
"deleted" || | |||||
// !!formProps.getValues("projectActualStart") && | |||||
!!( | |||||
formProps.getValues("projectStatus")?.toLowerCase() === | |||||
"completed" | |||||
) | |||||
} | } | ||||
> | > | ||||
{isEditMode ? t("Save") : t("Confirm")} | {isEditMode ? t("Save") : t("Confirm")} | ||||
@@ -12,7 +12,11 @@ import { | |||||
fetchProjectWorkNatures, | fetchProjectWorkNatures, | ||||
} from "@/app/api/projects"; | } from "@/app/api/projects"; | ||||
import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | ||||
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | |||||
import { | |||||
fetchAllCustomers, | |||||
fetchAllSubsidiaries, | |||||
fetchCustomerTypes, | |||||
} from "@/app/api/customer"; | |||||
import { fetchGrades } from "@/app/api/grades"; | import { fetchGrades } from "@/app/api/grades"; | ||||
import { getUserAbilities } from "@/app/utils/commonUtil"; | import { getUserAbilities } from "@/app/utils/commonUtil"; | ||||
@@ -44,6 +48,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
workNatures, | workNatures, | ||||
allStaffs, | allStaffs, | ||||
grades, | grades, | ||||
customerTypes, | |||||
abilities, | abilities, | ||||
] = await Promise.all([ | ] = await Promise.all([ | ||||
fetchAllTasks(), | fetchAllTasks(), | ||||
@@ -60,6 +65,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
fetchProjectWorkNatures(), | fetchProjectWorkNatures(), | ||||
fetchStaff(), | fetchStaff(), | ||||
fetchGrades(), | fetchGrades(), | ||||
fetchCustomerTypes(), | |||||
getUserAbilities(), | getUserAbilities(), | ||||
]); | ]); | ||||
@@ -90,6 +96,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
workNatures={workNatures} | workNatures={workNatures} | ||||
allStaffs={allStaffs} | allStaffs={allStaffs} | ||||
grades={grades} | grades={grades} | ||||
customerTypes={customerTypes} | |||||
mainProjects={mainProjects} | mainProjects={mainProjects} | ||||
abilities={abilities} | abilities={abilities} | ||||
/> | /> | ||||
@@ -4,18 +4,12 @@ import Stack from "@mui/material/Stack"; | |||||
import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||
import Card from "@mui/material/Card"; | import Card from "@mui/material/Card"; | ||||
import CardContent from "@mui/material/CardContent"; | import CardContent from "@mui/material/CardContent"; | ||||
import FormControl from "@mui/material/FormControl"; | |||||
import Grid from "@mui/material/Grid"; | import Grid from "@mui/material/Grid"; | ||||
import InputLabel from "@mui/material/InputLabel"; | |||||
import MenuItem from "@mui/material/MenuItem"; | |||||
import Select from "@mui/material/Select"; | |||||
import TextField from "@mui/material/TextField"; | import TextField from "@mui/material/TextField"; | ||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import CardActions from "@mui/material/CardActions"; | |||||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
import { Controller, useFormContext } from "react-hook-form"; | |||||
import { useFormContext } from "react-hook-form"; | |||||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | import { CreateProjectInputs } from "@/app/api/projects/actions"; | ||||
import { | import { | ||||
BuildingType, | BuildingType, | ||||
@@ -28,11 +22,15 @@ import { | |||||
WorkNature, | WorkNature, | ||||
} from "@/app/api/projects"; | } from "@/app/api/projects"; | ||||
import { StaffResult } from "@/app/api/staff"; | import { StaffResult } from "@/app/api/staff"; | ||||
import { Contact, Customer, Subsidiary } from "@/app/api/customer"; | |||||
import { | |||||
Contact, | |||||
Customer, | |||||
CustomerType, | |||||
Subsidiary, | |||||
} from "@/app/api/customer"; | |||||
import Link from "next/link"; | import Link from "next/link"; | ||||
import React, { useEffect, useMemo, useState } from "react"; | import React, { useEffect, useMemo, useState } from "react"; | ||||
import { fetchCustomer } from "@/app/api/customer/actions"; | import { fetchCustomer } from "@/app/api/customer/actions"; | ||||
import { Autocomplete, Checkbox, ListItemText } from "@mui/material"; | |||||
import uniq from "lodash/uniq"; | import uniq from "lodash/uniq"; | ||||
import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComplete"; | import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComplete"; | ||||
@@ -51,6 +49,7 @@ interface Props { | |||||
locationTypes: LocationType[]; | locationTypes: LocationType[]; | ||||
buildingTypes: BuildingType[]; | buildingTypes: BuildingType[]; | ||||
workNatures: WorkNature[]; | workNatures: WorkNature[]; | ||||
customerTypes: CustomerType[]; | |||||
} | } | ||||
const ProjectClientDetails: React.FC<Props> = ({ | const ProjectClientDetails: React.FC<Props> = ({ | ||||
@@ -67,6 +66,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
fundingTypes, | fundingTypes, | ||||
locationTypes, | locationTypes, | ||||
buildingTypes, | buildingTypes, | ||||
customerTypes, | |||||
workNatures, | workNatures, | ||||
}) => { | }) => { | ||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
@@ -89,10 +89,6 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
); | ); | ||||
const selectedCustomerId = watch("clientId"); | const selectedCustomerId = watch("clientId"); | ||||
const selectedCustomer = useMemo( | |||||
() => allCustomers.find((c) => c.id === selectedCustomerId), | |||||
[allCustomers, selectedCustomerId], | |||||
); | |||||
const [customerContacts, setCustomerContacts] = useState<Contact[]>([]); | const [customerContacts, setCustomerContacts] = useState<Contact[]>([]); | ||||
const [subsidiaryContacts, setSubsidiaryContacts] = useState<Contact[]>([]); | const [subsidiaryContacts, setSubsidiaryContacts] = useState<Contact[]>([]); | ||||
@@ -103,44 +99,77 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
const selectedCustomerContactId = watch("clientContactId"); | const selectedCustomerContactId = watch("clientContactId"); | ||||
const selectedCustomerContact = useMemo( | const selectedCustomerContact = useMemo( | ||||
() => | () => | ||||
subsidiaryContacts.length > 0 ? | |||||
subsidiaryContacts.find((contact) => contact.id === selectedCustomerContactId) | |||||
subsidiaryContacts.length > 0 | |||||
? subsidiaryContacts.find( | |||||
(contact) => contact.id === selectedCustomerContactId, | |||||
) | |||||
: customerContacts.find( | : customerContacts.find( | ||||
(contact) => contact.id === selectedCustomerContactId, | |||||
), | |||||
(contact) => contact.id === selectedCustomerContactId, | |||||
), | |||||
[subsidiaryContacts, customerContacts, selectedCustomerContactId], | [subsidiaryContacts, customerContacts, selectedCustomerContactId], | ||||
); | ); | ||||
// get customer (client) contact combo | // get customer (client) contact combo | ||||
const clientSubsidiaryId = watch("clientSubsidiaryId") | |||||
const [firstCustomerLoaded, setFirstCustomerLoaded] = useState(false) | |||||
const clientSubsidiaryId = watch("clientSubsidiaryId"); | |||||
const [firstCustomerLoaded, setFirstCustomerLoaded] = useState(false); | |||||
useEffect(() => { | useEffect(() => { | ||||
if (selectedCustomerId !== undefined) { | if (selectedCustomerId !== undefined) { | ||||
fetchCustomer(selectedCustomerId).then(({ contacts, subsidiaryIds }) => { | |||||
setCustomerContacts(contacts); | |||||
setCustomerSubsidiaryIds(subsidiaryIds); | |||||
fetchCustomer(selectedCustomerId).then( | |||||
({ contacts, subsidiaryIds, customer }) => { | |||||
setCustomerContacts(contacts); | |||||
setCustomerSubsidiaryIds(subsidiaryIds); | |||||
if (isEditMode && firstCustomerLoaded) { | |||||
setValue("clientTypeId", customer.customerType.id); | |||||
} | |||||
// if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0]) | |||||
// else | |||||
if (isEditMode && !firstCustomerLoaded) { setFirstCustomerLoaded(true) } | |||||
else if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", clientSubsidiaryId !== undefined && clientSubsidiaryId !== null ? subsidiaryIds.includes(clientSubsidiaryId) ? clientSubsidiaryId : null : null) | |||||
// if (contacts.length > 0) setValue("clientContactId", contacts[0].id) | |||||
// else setValue("clientContactId", undefined) | |||||
}); | |||||
if (isEditMode && !firstCustomerLoaded) { | |||||
setFirstCustomerLoaded(true); | |||||
} else if (subsidiaryIds.length > 0) | |||||
setValue( | |||||
"clientSubsidiaryId", | |||||
clientSubsidiaryId !== undefined && clientSubsidiaryId !== null | |||||
? subsidiaryIds.includes(clientSubsidiaryId) | |||||
? clientSubsidiaryId | |||||
: null | |||||
: null, | |||||
); | |||||
// if (contacts.length > 0) setValue("clientContactId", contacts[0].id) | |||||
// else setValue("clientContactId", undefined) | |||||
}, | |||||
); | |||||
} | } | ||||
}, [selectedCustomerId]); | }, [selectedCustomerId]); | ||||
useEffect(() => { | useEffect(() => { | ||||
if (Boolean(clientSubsidiaryId)) { | if (Boolean(clientSubsidiaryId)) { | ||||
// get subsidiary contact combo | // get subsidiary contact combo | ||||
const contacts = allSubsidiaries.find(subsidiary => subsidiary.id === clientSubsidiaryId)?.subsidiaryContacts!! | |||||
setSubsidiaryContacts(() => contacts) | |||||
setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && Boolean(defaultValues?.clientSubsidiaryId) ? contacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? contacts[0].id : contacts[0].id) | |||||
setValue("isSubsidiaryContact", true) | |||||
const contacts = allSubsidiaries.find( | |||||
(subsidiary) => subsidiary.id === clientSubsidiaryId, | |||||
)!.subsidiaryContacts; | |||||
setSubsidiaryContacts(() => contacts); | |||||
setValue( | |||||
"clientContactId", | |||||
selectedCustomerId === defaultValues?.clientId && | |||||
Boolean(defaultValues?.clientSubsidiaryId) | |||||
? contacts.find( | |||||
(contact) => contact.id === defaultValues.clientContactId, | |||||
)?.id ?? contacts[0].id | |||||
: contacts[0].id, | |||||
); | |||||
setValue("isSubsidiaryContact", true); | |||||
} else if (customerContacts?.length > 0) { | } else if (customerContacts?.length > 0) { | ||||
setSubsidiaryContacts(() => []) | |||||
setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && !Boolean(defaultValues?.clientSubsidiaryId) ? customerContacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? customerContacts[0].id : customerContacts[0].id) | |||||
setValue("isSubsidiaryContact", false) | |||||
setSubsidiaryContacts(() => []); | |||||
setValue( | |||||
"clientContactId", | |||||
selectedCustomerId === defaultValues?.clientId && | |||||
!Boolean(defaultValues?.clientSubsidiaryId) | |||||
? customerContacts.find( | |||||
(contact) => contact.id === defaultValues.clientContactId, | |||||
)?.id ?? customerContacts[0].id | |||||
: customerContacts[0].id, | |||||
); | |||||
setValue("isSubsidiaryContact", false); | |||||
} | } | ||||
}, [customerContacts, clientSubsidiaryId, selectedCustomerId]); | }, [customerContacts, clientSubsidiaryId, selectedCustomerId]); | ||||
@@ -155,31 +184,37 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
}, [getValues, selectedTeamLeadId, setValue]); | }, [getValues, selectedTeamLeadId, setValue]); | ||||
// Automatically update the project & client details whene select a main project | // Automatically update the project & client details whene select a main project | ||||
const mainProjectId = watch("mainProjectId") | |||||
const mainProjectId = watch("mainProjectId"); | |||||
useEffect(() => { | useEffect(() => { | ||||
if (mainProjectId !== undefined && mainProjects !== undefined && !isEditMode) { | |||||
const mainProject = mainProjects.find(project => project.projectId === mainProjectId); | |||||
if ( | |||||
mainProjectId !== undefined && | |||||
mainProjects !== undefined && | |||||
!isEditMode | |||||
) { | |||||
const mainProject = mainProjects.find( | |||||
(project) => project.projectId === mainProjectId, | |||||
); | |||||
if (mainProject !== undefined) { | if (mainProject !== undefined) { | ||||
setValue("projectName", mainProject.projectName) | |||||
setValue("projectCategoryId", mainProject.projectCategoryId) | |||||
setValue("projectLeadId", mainProject.projectLeadId) | |||||
setValue("serviceTypeId", mainProject.serviceTypeId) | |||||
setValue("fundingTypeId", mainProject.fundingTypeId) | |||||
setValue("contractTypeId", mainProject.contractTypeId) | |||||
setValue("locationId", mainProject.locationId) | |||||
setValue("buildingTypeIds", mainProject.buildingTypeIds) | |||||
setValue("workNatureIds", mainProject.workNatureIds) | |||||
setValue("projectDescription", mainProject.projectDescription) | |||||
setValue("expectedProjectFee", mainProject.expectedProjectFee) | |||||
setValue("subContractFee", mainProject.subContractFee) | |||||
setValue("isClpProject", mainProject.isClpProject) | |||||
setValue("clientId", mainProject.clientId) | |||||
setValue("clientSubsidiaryId", mainProject.clientSubsidiaryId) | |||||
setValue("clientContactId", mainProject.clientContactId) | |||||
setValue("projectName", mainProject.projectName); | |||||
setValue("projectCategoryId", mainProject.projectCategoryId); | |||||
setValue("projectLeadId", mainProject.projectLeadId); | |||||
setValue("serviceTypeId", mainProject.serviceTypeId); | |||||
setValue("fundingTypeId", mainProject.fundingTypeId); | |||||
setValue("contractTypeId", mainProject.contractTypeId); | |||||
setValue("locationId", mainProject.locationId); | |||||
setValue("buildingTypeIds", mainProject.buildingTypeIds); | |||||
setValue("workNatureIds", mainProject.workNatureIds); | |||||
setValue("projectDescription", mainProject.projectDescription); | |||||
setValue("expectedProjectFee", mainProject.expectedProjectFee); | |||||
setValue("subContractFee", mainProject.subContractFee); | |||||
setValue("isClpProject", mainProject.isClpProject); | |||||
setValue("clientId", mainProject.clientId); | |||||
setValue("clientSubsidiaryId", mainProject.clientSubsidiaryId); | |||||
setValue("clientContactId", mainProject.clientContactId); | |||||
} | } | ||||
} | } | ||||
}, [getValues, mainProjectId, setValue, isEditMode]) | |||||
}, [getValues, mainProjectId, setValue, isEditMode]); | |||||
// const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>( | // const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>( | ||||
// (acc, building) => ({ ...acc, [building.id]: building.name }), | // (acc, building) => ({ ...acc, [building.id]: building.name }), | ||||
@@ -199,29 +234,36 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
{t("Project Details")} | {t("Project Details")} | ||||
</Typography> | </Typography> | ||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | ||||
{ | |||||
isSubProject && mainProjects !== undefined && <><Grid item xs={6}> | |||||
<ControlledAutoComplete | |||||
control={control} | |||||
options={[...mainProjects.map(mainProject => ({ id: mainProject.projectId, label: `${mainProject.projectCode} - ${mainProject.projectName}` }))]} | |||||
name="mainProjectId" | |||||
label={t("Main Project")} | |||||
noOptionsText={t("No Main Project")} | |||||
disabled={isEditMode} | |||||
/> | |||||
</Grid> | |||||
<Grid item sx={{ display: { xs: "none", sm: "block" } }} /></> | |||||
} | |||||
{isSubProject && mainProjects !== undefined && ( | |||||
<> | |||||
<Grid item xs={6}> | |||||
<ControlledAutoComplete | |||||
control={control} | |||||
options={[ | |||||
...mainProjects.map((mainProject) => ({ | |||||
id: mainProject.projectId, | |||||
label: `${mainProject.projectCode} - ${mainProject.projectName}`, | |||||
})), | |||||
]} | |||||
name="mainProjectId" | |||||
label={t("Main Project")} | |||||
noOptionsText={t("No Main Project")} | |||||
disabled={isEditMode} | |||||
/> | |||||
</Grid> | |||||
<Grid item sx={{ display: { xs: "none", sm: "block" } }} /> | |||||
</> | |||||
)} | |||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("Project Code")} | label={t("Project Code")} | ||||
fullWidth | fullWidth | ||||
disabled={isSubProject && mainProjects !== undefined} | disabled={isSubProject && mainProjects !== undefined} | ||||
{...register("projectCode", | |||||
{ | |||||
required: !(isSubProject && mainProjects !== undefined) && "Project code required!", | |||||
} | |||||
)} | |||||
{...register("projectCode", { | |||||
required: | |||||
!(isSubProject && mainProjects !== undefined) && | |||||
"Project code required!", | |||||
})} | |||||
error={Boolean(errors.projectCode)} | error={Boolean(errors.projectCode)} | ||||
/> | /> | ||||
</Grid> | </Grid> | ||||
@@ -247,7 +289,10 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<ControlledAutoComplete | <ControlledAutoComplete | ||||
control={control} | control={control} | ||||
options={teamLeads.map((staff) => ({ ...staff, label: `${staff.staffId} - ${staff.name} (${staff.team})` }))} | |||||
options={teamLeads.map((staff) => ({ | |||||
...staff, | |||||
label: `${staff.staffId} - ${staff.name} (${staff.team})`, | |||||
}))} | |||||
name="projectLeadId" | name="projectLeadId" | ||||
label={t("Team Lead")} | label={t("Team Lead")} | ||||
noOptionsText={t("No Team Lead")} | noOptionsText={t("No Team Lead")} | ||||
@@ -338,7 +383,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
type="number" | type="number" | ||||
inputProps={{ step: "0.01" }} | inputProps={{ step: "0.01" }} | ||||
InputLabelProps={{ | InputLabelProps={{ | ||||
shrink: Boolean(watch("subContractFee")) | |||||
shrink: Boolean(watch("subContractFee")), | |||||
}} | }} | ||||
{...register("subContractFee", { valueAsNumber: true })} | {...register("subContractFee", { valueAsNumber: true })} | ||||
/> | /> | ||||
@@ -375,24 +420,29 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<ControlledAutoComplete | <ControlledAutoComplete | ||||
control={control} | control={control} | ||||
options={allCustomers.map((customer) => ({ ...customer, label: `${customer.code} - ${customer.name}` }))} | |||||
options={allCustomers.map((customer) => ({ | |||||
...customer, | |||||
label: `${customer.code} - ${customer.name}`, | |||||
}))} | |||||
name="clientId" | name="clientId" | ||||
label={t("Client")} | label={t("Client")} | ||||
noOptionsText={t("No Client")} | noOptionsText={t("No Client")} | ||||
rules={{ | rules={{ | ||||
required: "Please select a client" | |||||
required: "Please select a client", | |||||
}} | }} | ||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item sx={{ display: { xs: "none", sm: "block" } }} /> | <Grid item sx={{ display: { xs: "none", sm: "block" } }} /> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | |||||
<ControlledAutoComplete | |||||
control={control} | |||||
options={customerTypes} | |||||
name="clientTypeId" | |||||
label={t("Client Type")} | label={t("Client Type")} | ||||
InputProps={{ | |||||
readOnly: true, | |||||
noOptionsText={t("No Client Type")} | |||||
rules={{ | |||||
required: "Please select a client type", | |||||
}} | }} | ||||
fullWidth | |||||
value={selectedCustomer?.customerType.name || ""} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item sx={{ display: { xs: "none", sm: "block" } }} /> | <Grid item sx={{ display: { xs: "none", sm: "block" } }} /> | ||||
@@ -401,12 +451,18 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<ControlledAutoComplete | <ControlledAutoComplete | ||||
control={control} | control={control} | ||||
options={[{ label: t("No Subsidiary") }, ...customerSubsidiaryIds | |||||
.filter((subId) => subsidiaryMap[subId]) | |||||
.map((subsidiaryId, index) => { | |||||
const subsidiary = subsidiaryMap[subsidiaryId] | |||||
return { id: subsidiary.id, label: `${subsidiary.code} - ${subsidiary.name}` } | |||||
})]} | |||||
options={[ | |||||
{ label: t("No Subsidiary") }, | |||||
...customerSubsidiaryIds | |||||
.filter((subId) => subsidiaryMap[subId]) | |||||
.map((subsidiaryId, index) => { | |||||
const subsidiary = subsidiaryMap[subsidiaryId]; | |||||
return { | |||||
id: subsidiary.id, | |||||
label: `${subsidiary.code} - ${subsidiary.name}`, | |||||
}; | |||||
}), | |||||
]} | |||||
name="clientSubsidiaryId" | name="clientSubsidiaryId" | ||||
label={t("Client Subsidiary")} | label={t("Client Subsidiary")} | ||||
noOptionsText={t("No Client Subsidiary")} | noOptionsText={t("No Client Subsidiary")} | ||||
@@ -415,18 +471,25 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<ControlledAutoComplete | <ControlledAutoComplete | ||||
control={control} | control={control} | ||||
options={Boolean(watch("clientSubsidiaryId")) ? subsidiaryContacts : customerContacts} | |||||
options={ | |||||
Boolean(watch("clientSubsidiaryId")) | |||||
? subsidiaryContacts | |||||
: customerContacts | |||||
} | |||||
name="clientContactId" | name="clientContactId" | ||||
label={t("Client Lead")} | label={t("Client Lead")} | ||||
noOptionsText={t("No Client Lead")} | noOptionsText={t("No Client Lead")} | ||||
rules={{ | rules={{ | ||||
validate: (value) => { | validate: (value) => { | ||||
if ( | if ( | ||||
(customerContacts.length > 0 && !customerContacts.find( | |||||
customerContacts.length > 0 && | |||||
!customerContacts.find( | |||||
(contact) => contact.id === value, | (contact) => contact.id === value, | ||||
)) && (subsidiaryContacts?.length > 0 && !subsidiaryContacts.find( | |||||
) && | |||||
subsidiaryContacts?.length > 0 && | |||||
!subsidiaryContacts.find( | |||||
(contact) => contact.id === value, | (contact) => contact.id === value, | ||||
)) | |||||
) | |||||
) { | ) { | ||||
return t("Please provide a valid contact"); | return t("Please provide a valid contact"); | ||||
} else return true; | } else return true; | ||||