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