@@ -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 { | |||
fetchProjectBuildingTypes, | |||
@@ -26,9 +26,9 @@ export const metadata: Metadata = { | |||
const Projects: React.FC = async () => { | |||
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(); | |||
} | |||
@@ -44,6 +44,7 @@ const Projects: React.FC = async () => { | |||
fetchProjectWorkNatures(); | |||
fetchAllCustomers(); | |||
fetchAllSubsidiaries(); | |||
fetchCustomerTypes(); | |||
fetchGrades(); | |||
preloadTeamLeads(); | |||
preloadStaff(); | |||
@@ -40,6 +40,7 @@ export interface CreateProjectInputs { | |||
clientSubsidiaryId?: number | null; | |||
subsidiaryContactId?: number; | |||
isSubsidiaryContact?: boolean; | |||
clientTypeId?: number; | |||
// Allocation | |||
totalManhour: number; | |||
@@ -117,12 +118,12 @@ export const deleteProject = async (id: number) => { | |||
}; | |||
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; | |||
@@ -41,7 +41,7 @@ import { | |||
import { StaffResult } from "@/app/api/staff"; | |||
import { Typography } from "@mui/material"; | |||
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 { | |||
deleteDialog, | |||
@@ -70,6 +70,7 @@ export interface Props { | |||
buildingTypes: BuildingType[]; | |||
workNatures: WorkNature[]; | |||
allStaffs: StaffResult[]; | |||
customerTypes: CustomerType[]; | |||
grades: Grade[]; | |||
abilities: string[]; | |||
} | |||
@@ -81,16 +82,19 @@ const hasErrorsInTab = ( | |||
switch (tabIndex) { | |||
case 0: | |||
return ( | |||
errors.projectName || errors.projectDescription || errors.clientId || errors.projectCode | |||
errors.projectName || | |||
errors.projectDescription || | |||
errors.clientId || | |||
errors.projectCode | |||
); | |||
case 2: | |||
return ( | |||
errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups | |||
errors.totalManhour || | |||
errors.manhourPercentageByGrade || | |||
errors.taskGroups | |||
); | |||
case 3: | |||
return ( | |||
errors.milestones | |||
) | |||
return errors.milestones; | |||
default: | |||
false; | |||
} | |||
@@ -115,6 +119,7 @@ const CreateProject: React.FC<Props> = ({ | |||
buildingTypes, | |||
workNatures, | |||
allStaffs, | |||
customerTypes, | |||
abilities, | |||
}) => { | |||
const [serverError, setServerError] = useState(""); | |||
@@ -151,53 +156,102 @@ const CreateProject: React.FC<Props> = ({ | |||
console.log(data); | |||
// detect errors | |||
let hasErrors = false | |||
let hasErrors = false; | |||
// Tab - Staff Allocation and Resource | |||
if (data.totalManhour === null || data.totalManhour <= 0) { | |||
formProps.setError("totalManhour", { message: "totalManhour value is not valid", type: "required" }) | |||
setTabIndex(2) | |||
hasErrors = true | |||
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 | |||
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 | |||
setServerError(""); | |||
@@ -227,18 +281,29 @@ const CreateProject: React.FC<Props> = ({ | |||
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); | |||
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(() => { | |||
router.replace("/projects"); | |||
}); | |||
} else { | |||
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; | |||
@@ -257,7 +322,7 @@ const CreateProject: React.FC<Props> = ({ | |||
const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | |||
(errors) => { | |||
console.log(errors) | |||
console.log(errors); | |||
// Set the tab so that the focus will go there | |||
if ( | |||
errors.projectName || | |||
@@ -266,10 +331,14 @@ const CreateProject: React.FC<Props> = ({ | |||
errors.clientId | |||
) { | |||
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) { | |||
setTabIndex(3) | |||
setTabIndex(3); | |||
} | |||
}, | |||
[], | |||
@@ -282,18 +351,26 @@ const CreateProject: React.FC<Props> = ({ | |||
milestones: {}, | |||
totalManhour: 0, | |||
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, | |||
...defaultInputs, | |||
// manhourPercentageByGrade should have a sensible default | |||
manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade) | |||
? grades.reduce((acc, grade) => { | |||
return { ...acc, [grade.id]: 100 / grades.length }; | |||
}, {}) | |||
return { ...acc, [grade.id]: 100 / grades.length }; | |||
}, {}) | |||
: defaultInputs?.manhourPercentageByGrade, | |||
}, | |||
}); | |||
@@ -311,7 +388,8 @@ const CreateProject: React.FC<Props> = ({ | |||
{isEditMode && !(formProps.getValues("projectDeleted") === true) && ( | |||
<Stack direction="row" gap={1}> | |||
{/* {!formProps.getValues("projectActualStart") && ( */} | |||
{formProps.getValues("projectStatus")?.toLowerCase() === "pending to start" && ( | |||
{formProps.getValues("projectStatus")?.toLowerCase() === | |||
"pending to start" && ( | |||
<Button | |||
name="start" | |||
type="submit" | |||
@@ -324,7 +402,8 @@ const CreateProject: React.FC<Props> = ({ | |||
)} | |||
{/* {formProps.getValues("projectActualStart") && | |||
!formProps.getValues("projectActualEnd") && ( */} | |||
{formProps.getValues("projectStatus")?.toLowerCase() === "on-going" && ( | |||
{formProps.getValues("projectStatus")?.toLowerCase() === | |||
"on-going" && ( | |||
<Button | |||
name="complete" | |||
type="submit" | |||
@@ -338,9 +417,14 @@ const CreateProject: React.FC<Props> = ({ | |||
{!( | |||
// formProps.getValues("projectActualStart") && | |||
// 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 | |||
variant="outlined" | |||
startIcon={<Delete />} | |||
@@ -359,7 +443,13 @@ 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 }} | |||
sx={{ | |||
marginInlineEnd: | |||
!hasErrorsInTab(1, errors) && | |||
(hasErrorsInTab(2, errors) || hasErrorsInTab(3, errors)) | |||
? 1 | |||
: undefined, | |||
}} | |||
icon={ | |||
hasErrorsInTab(0, errors) ? ( | |||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||
@@ -369,11 +459,22 @@ const CreateProject: React.FC<Props> = ({ | |||
/> | |||
<Tab | |||
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 | |||
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={ | |||
hasErrorsInTab(2, errors) ? ( | |||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||
@@ -381,12 +482,15 @@ const CreateProject: React.FC<Props> = ({ | |||
} | |||
iconPosition="end" | |||
/> | |||
<Tab label={t("Milestone")} | |||
<Tab | |||
label={t("Milestone")} | |||
icon={ | |||
hasErrorsInTab(3, errors) ? ( | |||
<Error sx={{ marginInlineEnd: 1 }} color="error" />) | |||
: undefined} | |||
iconPosition="end" /> | |||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||
) : undefined | |||
} | |||
iconPosition="end" | |||
/> | |||
</Tabs> | |||
{ | |||
<ProjectClientDetails | |||
@@ -401,6 +505,7 @@ const CreateProject: React.FC<Props> = ({ | |||
allCustomers={allCustomers} | |||
allSubsidiaries={allSubsidiaries} | |||
projectCategories={projectCategories} | |||
customerTypes={customerTypes} | |||
teamLeads={teamLeads} | |||
isActive={tabIndex === 0} | |||
isEditMode={isEditMode} | |||
@@ -440,11 +545,14 @@ const CreateProject: React.FC<Props> = ({ | |||
startIcon={<Check />} | |||
type="submit" | |||
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")} | |||
@@ -12,7 +12,11 @@ import { | |||
fetchProjectWorkNatures, | |||
} from "@/app/api/projects"; | |||
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 { getUserAbilities } from "@/app/utils/commonUtil"; | |||
@@ -44,6 +48,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||
workNatures, | |||
allStaffs, | |||
grades, | |||
customerTypes, | |||
abilities, | |||
] = await Promise.all([ | |||
fetchAllTasks(), | |||
@@ -60,6 +65,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||
fetchProjectWorkNatures(), | |||
fetchStaff(), | |||
fetchGrades(), | |||
fetchCustomerTypes(), | |||
getUserAbilities(), | |||
]); | |||
@@ -90,6 +96,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||
workNatures={workNatures} | |||
allStaffs={allStaffs} | |||
grades={grades} | |||
customerTypes={customerTypes} | |||
mainProjects={mainProjects} | |||
abilities={abilities} | |||
/> | |||
@@ -4,18 +4,12 @@ import Stack from "@mui/material/Stack"; | |||
import Box from "@mui/material/Box"; | |||
import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import FormControl from "@mui/material/FormControl"; | |||
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 Typography from "@mui/material/Typography"; | |||
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 { Controller, useFormContext } from "react-hook-form"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
import { | |||
BuildingType, | |||
@@ -28,11 +22,15 @@ import { | |||
WorkNature, | |||
} from "@/app/api/projects"; | |||
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 React, { useEffect, useMemo, useState } from "react"; | |||
import { fetchCustomer } from "@/app/api/customer/actions"; | |||
import { Autocomplete, Checkbox, ListItemText } from "@mui/material"; | |||
import uniq from "lodash/uniq"; | |||
import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComplete"; | |||
@@ -51,6 +49,7 @@ interface Props { | |||
locationTypes: LocationType[]; | |||
buildingTypes: BuildingType[]; | |||
workNatures: WorkNature[]; | |||
customerTypes: CustomerType[]; | |||
} | |||
const ProjectClientDetails: React.FC<Props> = ({ | |||
@@ -67,6 +66,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
fundingTypes, | |||
locationTypes, | |||
buildingTypes, | |||
customerTypes, | |||
workNatures, | |||
}) => { | |||
const { t } = useTranslation(); | |||
@@ -89,10 +89,6 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
); | |||
const selectedCustomerId = watch("clientId"); | |||
const selectedCustomer = useMemo( | |||
() => allCustomers.find((c) => c.id === selectedCustomerId), | |||
[allCustomers, selectedCustomerId], | |||
); | |||
const [customerContacts, setCustomerContacts] = useState<Contact[]>([]); | |||
const [subsidiaryContacts, setSubsidiaryContacts] = useState<Contact[]>([]); | |||
@@ -103,44 +99,77 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
const selectedCustomerContactId = watch("clientContactId"); | |||
const selectedCustomerContact = useMemo( | |||
() => | |||
subsidiaryContacts.length > 0 ? | |||
subsidiaryContacts.find((contact) => contact.id === selectedCustomerContactId) | |||
subsidiaryContacts.length > 0 | |||
? subsidiaryContacts.find( | |||
(contact) => contact.id === selectedCustomerContactId, | |||
) | |||
: customerContacts.find( | |||
(contact) => contact.id === selectedCustomerContactId, | |||
), | |||
(contact) => contact.id === selectedCustomerContactId, | |||
), | |||
[subsidiaryContacts, customerContacts, selectedCustomerContactId], | |||
); | |||
// get customer (client) contact combo | |||
const clientSubsidiaryId = watch("clientSubsidiaryId") | |||
const [firstCustomerLoaded, setFirstCustomerLoaded] = useState(false) | |||
const clientSubsidiaryId = watch("clientSubsidiaryId"); | |||
const [firstCustomerLoaded, setFirstCustomerLoaded] = useState(false); | |||
useEffect(() => { | |||
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]); | |||
useEffect(() => { | |||
if (Boolean(clientSubsidiaryId)) { | |||
// 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) { | |||
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]); | |||
@@ -155,31 +184,37 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
}, [getValues, selectedTeamLeadId, setValue]); | |||
// Automatically update the project & client details whene select a main project | |||
const mainProjectId = watch("mainProjectId") | |||
const mainProjectId = watch("mainProjectId"); | |||
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) { | |||
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 }>( | |||
// (acc, building) => ({ ...acc, [building.id]: building.name }), | |||
@@ -199,29 +234,36 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
{t("Project Details")} | |||
</Typography> | |||
<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}> | |||
<TextField | |||
label={t("Project Code")} | |||
fullWidth | |||
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)} | |||
/> | |||
</Grid> | |||
@@ -247,7 +289,10 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
<Grid item xs={6}> | |||
<ControlledAutoComplete | |||
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" | |||
label={t("Team Lead")} | |||
noOptionsText={t("No Team Lead")} | |||
@@ -338,7 +383,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
type="number" | |||
inputProps={{ step: "0.01" }} | |||
InputLabelProps={{ | |||
shrink: Boolean(watch("subContractFee")) | |||
shrink: Boolean(watch("subContractFee")), | |||
}} | |||
{...register("subContractFee", { valueAsNumber: true })} | |||
/> | |||
@@ -375,24 +420,29 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
<Grid item xs={6}> | |||
<ControlledAutoComplete | |||
control={control} | |||
options={allCustomers.map((customer) => ({ ...customer, label: `${customer.code} - ${customer.name}` }))} | |||
options={allCustomers.map((customer) => ({ | |||
...customer, | |||
label: `${customer.code} - ${customer.name}`, | |||
}))} | |||
name="clientId" | |||
label={t("Client")} | |||
noOptionsText={t("No Client")} | |||
rules={{ | |||
required: "Please select a client" | |||
required: "Please select a client", | |||
}} | |||
/> | |||
</Grid> | |||
<Grid item sx={{ display: { xs: "none", sm: "block" } }} /> | |||
<Grid item xs={6}> | |||
<TextField | |||
<ControlledAutoComplete | |||
control={control} | |||
options={customerTypes} | |||
name="clientTypeId" | |||
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 item sx={{ display: { xs: "none", sm: "block" } }} /> | |||
@@ -401,12 +451,18 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
<Grid item xs={6}> | |||
<ControlledAutoComplete | |||
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" | |||
label={t("Client Subsidiary")} | |||
noOptionsText={t("No Client Subsidiary")} | |||
@@ -415,18 +471,25 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
<Grid item xs={6}> | |||
<ControlledAutoComplete | |||
control={control} | |||
options={Boolean(watch("clientSubsidiaryId")) ? subsidiaryContacts : customerContacts} | |||
options={ | |||
Boolean(watch("clientSubsidiaryId")) | |||
? subsidiaryContacts | |||
: customerContacts | |||
} | |||
name="clientContactId" | |||
label={t("Client Lead")} | |||
noOptionsText={t("No Client Lead")} | |||
rules={{ | |||
validate: (value) => { | |||
if ( | |||
(customerContacts.length > 0 && !customerContacts.find( | |||
customerContacts.length > 0 && | |||
!customerContacts.find( | |||
(contact) => contact.id === value, | |||
)) && (subsidiaryContacts?.length > 0 && !subsidiaryContacts.find( | |||
) && | |||
subsidiaryContacts?.length > 0 && | |||
!subsidiaryContacts.find( | |||
(contact) => contact.id === value, | |||
)) | |||
) | |||
) { | |||
return t("Please provide a valid contact"); | |||
} else return true; | |||