Browse Source

Allow changing client type

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 year ago
parent
commit
86dfc30972
5 changed files with 354 additions and 174 deletions
  1. +4
    -3
      src/app/(main)/projects/create/page.tsx
  2. +7
    -6
      src/app/api/projects/actions.ts
  3. +179
    -71
      src/components/CreateProject/CreateProject.tsx
  4. +8
    -1
      src/components/CreateProject/CreateProjectWrapper.tsx
  5. +156
    -93
      src/components/CreateProject/ProjectClientDetails.tsx

+ 4
- 3
src/app/(main)/projects/create/page.tsx View File

@@ -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();


+ 7
- 6
src/app/api/projects/actions.ts View File

@@ -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;


+ 179
- 71
src/components/CreateProject/CreateProject.tsx View File

@@ -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")}


+ 8
- 1
src/components/CreateProject/CreateProjectWrapper.tsx View File

@@ -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}
/> />


+ 156
- 93
src/components/CreateProject/ProjectClientDetails.tsx View File

@@ -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;


Loading…
Cancel
Save