@@ -1,6 +1,7 @@ | |||||
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | ||||
import { fetchGrades } from "@/app/api/grades"; | import { fetchGrades } from "@/app/api/grades"; | ||||
import { | import { | ||||
fetchMainProjects, | |||||
fetchProjectBuildingTypes, | fetchProjectBuildingTypes, | ||||
fetchProjectCategories, | fetchProjectCategories, | ||||
fetchProjectContractTypes, | fetchProjectContractTypes, | ||||
@@ -12,10 +13,12 @@ import { | |||||
} from "@/app/api/projects"; | } from "@/app/api/projects"; | ||||
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | ||||
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | ||||
import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||||
import CreateProject from "@/components/CreateProject"; | import CreateProject from "@/components/CreateProject"; | ||||
import { I18nProvider, getServerI18n } from "@/i18n"; | import { I18nProvider, getServerI18n } from "@/i18n"; | ||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
import { notFound } from "next/navigation"; | |||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
title: "Create Project", | title: "Create Project", | ||||
@@ -39,12 +42,21 @@ const Projects: React.FC = async () => { | |||||
fetchGrades(); | fetchGrades(); | ||||
preloadTeamLeads(); | preloadTeamLeads(); | ||||
preloadStaff(); | preloadStaff(); | ||||
try { | |||||
const data = await fetchMainProjects(); | |||||
if (!Boolean(data) || data.length === 0) { | |||||
notFound(); | |||||
} | |||||
} catch (e) { | |||||
notFound(); | |||||
} | |||||
return ( | return ( | ||||
<> | <> | ||||
<Typography variant="h4">{t("Create Sub Project")}</Typography> | <Typography variant="h4">{t("Create Sub Project")}</Typography> | ||||
<I18nProvider namespaces={["projects"]}> | <I18nProvider namespaces={["projects"]}> | ||||
<CreateProject isEditMode={false}/> | |||||
<CreateProject isEditMode={false} isSubProject={true} /> | |||||
</I18nProvider> | </I18nProvider> | ||||
</> | </> | ||||
); | ); | ||||
@@ -22,6 +22,7 @@ export interface CreateProjectInputs { | |||||
projectActualEnd: string; | projectActualEnd: string; | ||||
projectStatus: string; | projectStatus: string; | ||||
isClpProject: boolean; | isClpProject: boolean; | ||||
mainProjectId?: number | null; | |||||
// Project info | // Project info | ||||
serviceTypeId: number; | serviceTypeId: number; | ||||
@@ -15,6 +15,27 @@ export interface ProjectResult { | |||||
status: string; | status: string; | ||||
} | } | ||||
export interface MainProject { | |||||
projectId: number; | |||||
projectCode: string; | |||||
projectName: string; | |||||
projectCategoryId: number; | |||||
projectDescription: string; | |||||
projectLeadId: number; | |||||
projectStatus: string; | |||||
isClpProject: boolean; | |||||
serviceTypeId: number; | |||||
fundingTypeId: number; | |||||
contractTypeId: number; | |||||
locationId: number; | |||||
buildingTypeIds: number[]; | |||||
workNatureIds: number[]; | |||||
clientId: number; | |||||
clientContactId: number; | |||||
clientSubsidiaryId: number; | |||||
expectedProjectFee: number; | |||||
} | |||||
export interface ProjectCategory { | export interface ProjectCategory { | ||||
id: number; | id: number; | ||||
name: string; | name: string; | ||||
@@ -82,7 +103,7 @@ export const fetchProjects = cache(async () => { | |||||
}); | }); | ||||
export const fetchMainProjects = cache(async () => { | export const fetchMainProjects = cache(async () => { | ||||
return serverFetchJson<ProjectResult[]>(`${BASE_API_URL}/projects/main`, { | |||||
return serverFetchJson<MainProject[]>(`${BASE_API_URL}/projects/main`, { | |||||
next: { tags: ["projects"] }, | next: { tags: ["projects"] }, | ||||
}); | }); | ||||
}); | }); | ||||
@@ -15,7 +15,9 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
"/home": "User Workspace", | "/home": "User Workspace", | ||||
"/projects": "Projects", | "/projects": "Projects", | ||||
"/projects/create": "Create Project", | "/projects/create": "Create Project", | ||||
"/projects/create/sub": "Sub Project", | |||||
"/projects/edit": "Edit Project", | "/projects/edit": "Edit Project", | ||||
"/projects/edit/sub": "Sub Project", | |||||
"/tasks": "Task Template", | "/tasks": "Task Template", | ||||
"/tasks/create": "Create Task Template", | "/tasks/create": "Create Task Template", | ||||
"/staffReimbursement": "Staff Reimbursement", | "/staffReimbursement": "Staff Reimbursement", | ||||
@@ -10,7 +10,7 @@ const icon = <CheckBoxOutlineBlankIcon fontSize="medium" />; | |||||
const checkedIcon = <CheckBoxIcon fontSize="medium" />; | const checkedIcon = <CheckBoxIcon fontSize="medium" />; | ||||
// label -> e.g. code - name -> 001 - WL | // label -> e.g. code - name -> 001 - WL | ||||
// name -> WL | // name -> WL | ||||
interface Props<T extends { id?: number | string; label?: string; name?: string }, TField extends FieldValues> { | |||||
interface Props<T extends { id?: number | string | null; label?: string; name?: string }, TField extends FieldValues> { | |||||
control: Control<TField>, | control: Control<TField>, | ||||
options: T[], | options: T[], | ||||
name: Path<TField>, // register name | name: Path<TField>, // register name | ||||
@@ -18,7 +18,6 @@ interface Props<T extends { id?: number | string; label?: string; name?: string | |||||
noOptionsText?: string, | noOptionsText?: string, | ||||
isMultiple?: boolean, | isMultiple?: boolean, | ||||
rules?: RegisterOptions<FieldValues> | rules?: RegisterOptions<FieldValues> | ||||
error?: boolean, | |||||
} | } | ||||
function ControlledAutoComplete< | function ControlledAutoComplete< | ||||
@@ -28,68 +27,78 @@ function ControlledAutoComplete< | |||||
props: Props<T, TField> | props: Props<T, TField> | ||||
) { | ) { | ||||
const { t } = useTranslation() | const { t } = useTranslation() | ||||
const { control, options, name, label, noOptionsText, isMultiple, rules, error } = props; | |||||
const { control, options, name, label, noOptionsText, isMultiple, rules } = props; | |||||
// set default value if value is null | |||||
if (!Boolean(isMultiple) && !Boolean(control._formValues[name])) { | |||||
console.log(name, control._formValues[name]) | |||||
control._formValues[name] = options[0]?.id ?? undefined | |||||
} else if (Boolean(isMultiple) && !Boolean(control._formValues[name])) { | |||||
control._formValues[name] = [] | |||||
} | |||||
return ( | return ( | ||||
<Controller | <Controller | ||||
name={name} | name={name} | ||||
control={control} | control={control} | ||||
rules={rules} | rules={rules} | ||||
render={({ field }) => ( | |||||
isMultiple ? | |||||
<Autocomplete | |||||
multiple | |||||
disableClearable | |||||
disableCloseOnSelect | |||||
disablePortal | |||||
noOptionsText={noOptionsText ?? t("No Options")} | |||||
value={options.filter(option => { | |||||
// console.log(field.value) | |||||
return field.value?.includes(option.id) | |||||
})} | |||||
options={options} | |||||
getOptionLabel={(option) => option.label ?? option.name!!} | |||||
isOptionEqualToValue={(option, value) => option.id === value.id} | |||||
renderOption={(params, option, { selected }) => { | |||||
return ( | |||||
<li {...params}> | |||||
<Checkbox | |||||
icon={icon} | |||||
checkedIcon={checkedIcon} | |||||
checked={selected} | |||||
style={{ marginRight: 8 }} | |||||
/> | |||||
{option.label ?? option.name} | |||||
</li> | |||||
); | |||||
}} | |||||
onChange={(event, value) => { | |||||
field.onChange(value?.map(v => v.id)) | |||||
}} | |||||
renderInput={(params) => <TextField {...params} error={error} variant="outlined" label={label} />} | |||||
/> | |||||
: | |||||
<Autocomplete | |||||
disableClearable | |||||
disablePortal | |||||
noOptionsText={noOptionsText ?? t("No Options")} | |||||
value={options.find(option => option.id === field.value) ?? options[0]} | |||||
options={options} | |||||
getOptionLabel={(option) => option.label ?? option.name!!} | |||||
isOptionEqualToValue={(option, value) => option.id === value.id} | |||||
renderOption={(params, option) => { | |||||
return ( | |||||
<MenuItem {...params} key={option.id} value={option.id}> | |||||
{option.label ?? option.name} | |||||
</MenuItem> | |||||
); | |||||
}} | |||||
onChange={(event, value) => { | |||||
field.onChange(value?.id) | |||||
}} | |||||
renderInput={(params) => <TextField {...params} error={error} variant="outlined" label={label} />} | |||||
/> | |||||
)} | |||||
render={({ field, fieldState, formState }) => { | |||||
return ( | |||||
isMultiple ? | |||||
<Autocomplete | |||||
multiple | |||||
disableClearable | |||||
disableCloseOnSelect | |||||
disablePortal | |||||
noOptionsText={noOptionsText ?? t("No Options")} | |||||
value={options.filter(option => { | |||||
return field.value?.includes(option.id) | |||||
})} | |||||
options={options} | |||||
getOptionLabel={(option) => option.label ?? option.name!!} | |||||
isOptionEqualToValue={(option, value) => option.id === value.id} | |||||
renderOption={(params, option, { selected }) => { | |||||
return ( | |||||
<li {...params} key={option.id}> | |||||
<Checkbox | |||||
icon={icon} | |||||
checkedIcon={checkedIcon} | |||||
checked={selected} | |||||
style={{ marginRight: 8 }} | |||||
/> | |||||
{option.label ?? option.name} | |||||
</li> | |||||
); | |||||
}} | |||||
onChange={(event, value) => { | |||||
field.onChange(value?.map(v => v.id)) | |||||
}} | |||||
renderInput={(params) => <TextField {...params} error={Boolean(formState.errors[name])} variant="outlined" label={label} />} | |||||
/> | |||||
: | |||||
<Autocomplete | |||||
disableClearable | |||||
disablePortal | |||||
noOptionsText={noOptionsText ?? t("No Options")} | |||||
value={options.find(option => option.id === field.value) ?? options[0]} | |||||
options={options} | |||||
getOptionLabel={(option) => option.label ?? option.name!!} | |||||
isOptionEqualToValue={(option, value) => option?.id === value?.id} | |||||
renderOption={(params, option) => { | |||||
return ( | |||||
<MenuItem {...params} key={option.id} value={option.id}> | |||||
{option.label ?? option.name} | |||||
</MenuItem> | |||||
); | |||||
}} | |||||
onChange={(event, value) => { | |||||
field.onChange(value?.id) | |||||
}} | |||||
renderInput={(params) => <TextField {...params} error={Boolean(formState.errors[name])} variant="outlined" label={label} />} | |||||
/>) | |||||
}} | |||||
/> | /> | ||||
) | ) | ||||
} | } | ||||
@@ -33,6 +33,7 @@ import { | |||||
ContractType, | ContractType, | ||||
FundingType, | FundingType, | ||||
LocationType, | LocationType, | ||||
MainProject, | |||||
ProjectCategory, | ProjectCategory, | ||||
ServiceType, | ServiceType, | ||||
WorkNature, | WorkNature, | ||||
@@ -52,6 +53,8 @@ import dayjs from "dayjs"; | |||||
export interface Props { | export interface Props { | ||||
isEditMode: boolean; | isEditMode: boolean; | ||||
isSubProject: boolean; | |||||
mainProjects?: MainProject[]; | |||||
defaultInputs?: CreateProjectInputs; | defaultInputs?: CreateProjectInputs; | ||||
allTasks: Task[]; | allTasks: Task[]; | ||||
projectCategories: ProjectCategory[]; | projectCategories: ProjectCategory[]; | ||||
@@ -93,6 +96,8 @@ const hasErrorsInTab = ( | |||||
const CreateProject: React.FC<Props> = ({ | const CreateProject: React.FC<Props> = ({ | ||||
isEditMode, | isEditMode, | ||||
isSubProject, | |||||
mainProjects, | |||||
defaultInputs, | defaultInputs, | ||||
allTasks, | allTasks, | ||||
projectCategories, | projectCategories, | ||||
@@ -269,14 +274,9 @@ const CreateProject: React.FC<Props> = ({ | |||||
milestones: {}, | milestones: {}, | ||||
totalManhour: 0, | totalManhour: 0, | ||||
taskTemplateId: "All", | taskTemplateId: "All", | ||||
projectCategoryId: projectCategories.length > 0 ? projectCategories[0].id : undefined, | |||||
projectLeadId: teamLeads.length > 0 ? teamLeads[0].id : undefined, | |||||
serviceTypeId: serviceTypes.length > 0 ? serviceTypes[0].id : undefined, | |||||
fundingTypeId: fundingTypes.length > 0 ? fundingTypes[0].id : undefined, | |||||
contractTypeId: contractTypes.length > 0 ? contractTypes[0].id : undefined, | |||||
locationId: locationTypes.length > 0 ? locationTypes[0].id : undefined, | |||||
clientSubsidiaryId: undefined, | |||||
clientId: allCustomers.length > 0 ? allCustomers[0].id : undefined, | |||||
projectName: mainProjects !== undefined ? mainProjects[0].projectName : undefined, | |||||
projectDescription: mainProjects !== undefined ? mainProjects[0].projectDescription : undefined, | |||||
expectedProjectFee: mainProjects !== undefined ? mainProjects[0].expectedProjectFee : undefined, | |||||
...defaultInputs, | ...defaultInputs, | ||||
// manhourPercentageByGrade should have a sensible default | // manhourPercentageByGrade should have a sensible default | ||||
@@ -380,6 +380,8 @@ const CreateProject: React.FC<Props> = ({ | |||||
</Tabs> | </Tabs> | ||||
{ | { | ||||
<ProjectClientDetails | <ProjectClientDetails | ||||
isSubProject={isSubProject} | |||||
mainProjects={mainProjects} | |||||
buildingTypes={buildingTypes} | buildingTypes={buildingTypes} | ||||
workNatures={workNatures} | workNatures={workNatures} | ||||
contractTypes={contractTypes} | contractTypes={contractTypes} | ||||
@@ -1,6 +1,7 @@ | |||||
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | ||||
import CreateProject from "./CreateProject"; | import CreateProject from "./CreateProject"; | ||||
import { | import { | ||||
fetchMainProjects, | |||||
fetchProjectBuildingTypes, | fetchProjectBuildingTypes, | ||||
fetchProjectCategories, | fetchProjectCategories, | ||||
fetchProjectContractTypes, | fetchProjectContractTypes, | ||||
@@ -14,10 +15,14 @@ import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | |||||
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | ||||
import { fetchGrades } from "@/app/api/grades"; | import { fetchGrades } from "@/app/api/grades"; | ||||
type CreateProjectProps = { isEditMode: false }; | |||||
type CreateProjectProps = { | |||||
isEditMode: false; | |||||
isSubProject?: boolean; | |||||
}; | |||||
interface EditProjectProps { | interface EditProjectProps { | ||||
isEditMode: true; | isEditMode: true; | ||||
projectId?: string; | projectId?: string; | ||||
isSubProject?: boolean; | |||||
} | } | ||||
type Props = CreateProjectProps | EditProjectProps; | type Props = CreateProjectProps | EditProjectProps; | ||||
@@ -59,9 +64,14 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
? await fetchProjectDetails(props.projectId!) | ? await fetchProjectDetails(props.projectId!) | ||||
: undefined; | : undefined; | ||||
const mainProjects = Boolean(props.isSubProject) | |||||
? await fetchMainProjects() | |||||
: undefined; | |||||
return ( | return ( | ||||
<CreateProject | <CreateProject | ||||
isEditMode={props.isEditMode} | isEditMode={props.isEditMode} | ||||
isSubProject={Boolean(props.isSubProject)} | |||||
defaultInputs={projectInfo} | defaultInputs={projectInfo} | ||||
allTasks={tasks} | allTasks={tasks} | ||||
projectCategories={projectCategories} | projectCategories={projectCategories} | ||||
@@ -77,6 +87,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
workNatures={workNatures} | workNatures={workNatures} | ||||
allStaffs={allStaffs} | allStaffs={allStaffs} | ||||
grades={grades} | grades={grades} | ||||
mainProjects={mainProjects} | |||||
/> | /> | ||||
); | ); | ||||
}; | }; | ||||
@@ -22,6 +22,7 @@ import { | |||||
ContractType, | ContractType, | ||||
FundingType, | FundingType, | ||||
LocationType, | LocationType, | ||||
MainProject, | |||||
ProjectCategory, | ProjectCategory, | ||||
ServiceType, | ServiceType, | ||||
WorkNature, | WorkNature, | ||||
@@ -37,6 +38,8 @@ import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComp | |||||
interface Props { | interface Props { | ||||
isActive: boolean; | isActive: boolean; | ||||
isSubProject: boolean; | |||||
mainProjects?: MainProject[]; | |||||
projectCategories: ProjectCategory[]; | projectCategories: ProjectCategory[]; | ||||
teamLeads: StaffResult[]; | teamLeads: StaffResult[]; | ||||
allCustomers: Customer[]; | allCustomers: Customer[]; | ||||
@@ -51,6 +54,8 @@ interface Props { | |||||
const ProjectClientDetails: React.FC<Props> = ({ | const ProjectClientDetails: React.FC<Props> = ({ | ||||
isActive, | isActive, | ||||
isSubProject, | |||||
mainProjects, | |||||
projectCategories, | projectCategories, | ||||
teamLeads, | teamLeads, | ||||
allCustomers, | allCustomers, | ||||
@@ -70,6 +75,8 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
control, | control, | ||||
setValue, | setValue, | ||||
getValues, | getValues, | ||||
reset, | |||||
resetField, | |||||
} = useFormContext<CreateProjectInputs>(); | } = useFormContext<CreateProjectInputs>(); | ||||
const subsidiaryMap = useMemo<{ | const subsidiaryMap = useMemo<{ | ||||
@@ -136,7 +143,6 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
// Automatically add the team lead to the allocated staff list | // Automatically add the team lead to the allocated staff list | ||||
const selectedTeamLeadId = watch("projectLeadId"); | const selectedTeamLeadId = watch("projectLeadId"); | ||||
useEffect(() => { | useEffect(() => { | ||||
console.log(selectedTeamLeadId) | |||||
if (selectedTeamLeadId !== undefined) { | if (selectedTeamLeadId !== undefined) { | ||||
const currentStaffIds = getValues("allocatedStaffIds"); | const currentStaffIds = getValues("allocatedStaffIds"); | ||||
const newList = uniq([...currentStaffIds, selectedTeamLeadId]); | const newList = uniq([...currentStaffIds, selectedTeamLeadId]); | ||||
@@ -144,6 +150,32 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
} | } | ||||
}, [getValues, selectedTeamLeadId, setValue]); | }, [getValues, selectedTeamLeadId, setValue]); | ||||
// Automatically update the project & client details whene select a main project | |||||
const mainProjectId = watch("mainProjectId") | |||||
useEffect(() => { | |||||
if (mainProjectId !== undefined && mainProjects !== undefined) { | |||||
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("isClpProject", mainProject.isClpProject) | |||||
setValue("clientId", mainProject.clientId) | |||||
setValue("clientSubsidiaryId", mainProject.clientSubsidiaryId) | |||||
setValue("clientContactId", mainProject.clientContactId) | |||||
} | |||||
} | |||||
}, [getValues, mainProjectId, setValue]) | |||||
// 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 }), | ||||
// {}, | // {}, | ||||
@@ -162,6 +194,18 @@ 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")} | |||||
/> | |||||
</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")} | ||||
@@ -283,7 +327,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<Checkbox | <Checkbox | ||||
{...register("isClpProject")} | {...register("isClpProject")} | ||||
defaultChecked={watch("isClpProject")} | |||||
checked={Boolean(watch("isClpProject"))} | |||||
/> | /> | ||||
<Typography variant="overline" display="inline"> | <Typography variant="overline" display="inline"> | ||||
{t("CLP Project")} | {t("CLP Project")} | ||||
@@ -317,7 +361,6 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
rules={{ | rules={{ | ||||
required: "Please select a client" | required: "Please select a client" | ||||
}} | }} | ||||
error={Boolean(errors.clientId)} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item sx={{ display: { xs: "none", sm: "block" } }} /> | <Grid item sx={{ display: { xs: "none", sm: "block" } }} /> | ||||
@@ -337,7 +380,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<ControlledAutoComplete | <ControlledAutoComplete | ||||
control={control} | control={control} | ||||
options={[{ id: undefined, label: t("No Subsidiary") }, ...customerSubsidiaryIds | |||||
options={[{ label: t("No Subsidiary") }, ...customerSubsidiaryIds | |||||
.filter((subId) => subsidiaryMap[subId]) | .filter((subId) => subsidiaryMap[subId]) | ||||
.map((subsidiaryId, index) => { | .map((subsidiaryId, index) => { | ||||
const subsidiary = subsidiaryMap[subsidiaryId] | const subsidiary = subsidiaryMap[subsidiaryId] | ||||
@@ -368,7 +411,6 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
} else return true; | } else return true; | ||||
}, | }, | ||||
}} | }} | ||||
error={Boolean(errors.clientContactId)} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid container sx={{ display: { xs: "none", sm: "block" } }} /> | <Grid container sx={{ display: { xs: "none", sm: "block" } }} /> | ||||
@@ -25,6 +25,7 @@ import { | |||||
Tab, | Tab, | ||||
Tabs, | Tabs, | ||||
SelectChangeEvent, | SelectChangeEvent, | ||||
Autocomplete, | |||||
} from "@mui/material"; | } from "@mui/material"; | ||||
import differenceWith from "lodash/differenceWith"; | import differenceWith from "lodash/differenceWith"; | ||||
import intersectionWith from "lodash/intersectionWith"; | import intersectionWith from "lodash/intersectionWith"; | ||||
@@ -7,7 +7,7 @@ import Typography from "@mui/material/Typography"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import TransferList from "../TransferList"; | import TransferList from "../TransferList"; | ||||
import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
import React, { useCallback, useMemo, useState } from "react"; | |||||
import React, { SyntheticEvent, useCallback, useMemo, useState } from "react"; | |||||
import CardActions from "@mui/material/CardActions"; | import CardActions from "@mui/material/CardActions"; | ||||
import RestartAlt from "@mui/icons-material/RestartAlt"; | import RestartAlt from "@mui/icons-material/RestartAlt"; | ||||
import FormControl from "@mui/material/FormControl"; | import FormControl from "@mui/material/FormControl"; | ||||
@@ -20,6 +20,7 @@ import { CreateProjectInputs, ManhourAllocation } from "@/app/api/projects/actio | |||||
import isNumber from "lodash/isNumber"; | import isNumber from "lodash/isNumber"; | ||||
import intersectionWith from "lodash/intersectionWith"; | import intersectionWith from "lodash/intersectionWith"; | ||||
import { difference } from "lodash"; | import { difference } from "lodash"; | ||||
import { Autocomplete, TextField } from "@mui/material"; | |||||
interface Props { | interface Props { | ||||
allTasks: Task[]; | allTasks: Task[]; | ||||
@@ -50,9 +51,9 @@ const TaskSetup: React.FC<Props> = ({ | |||||
"All" | number | "All" | number | ||||
>(watch("taskTemplateId") ?? "All"); | >(watch("taskTemplateId") ?? "All"); | ||||
const onSelectTaskTemplate = useCallback( | const onSelectTaskTemplate = useCallback( | ||||
(e: SelectChangeEvent<number | "All">) => { | |||||
if (e.target.value === "All" || isNumber(e.target.value)) { | |||||
setSelectedTaskTemplateId(e.target.value); | |||||
(event: SyntheticEvent<Element, Event>, value: NonNullable<{id: number | string, name: string}>) => { | |||||
if (value.id === "All" || isNumber(value.id)) { | |||||
setSelectedTaskTemplateId(value.id); | |||||
// onReset(); | // onReset(); | ||||
} | } | ||||
}, | }, | ||||
@@ -132,21 +133,24 @@ const TaskSetup: React.FC<Props> = ({ | |||||
marginBlockEnd={1} | marginBlockEnd={1} | ||||
> | > | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<FormControl fullWidth> | |||||
<InputLabel>{t("Task List Source")}</InputLabel> | |||||
<Select<"All" | number> | |||||
label={t("Task List Source")} | |||||
value={selectedTaskTemplateId} | |||||
onChange={onSelectTaskTemplate} | |||||
> | |||||
<MenuItem value={"All"}>{t("All tasks")}</MenuItem> | |||||
{taskTemplates.map((template, index) => ( | |||||
<MenuItem key={`${template.id}-${index}`} value={template.id}> | |||||
{template.name} | |||||
<Autocomplete | |||||
disableClearable | |||||
disablePortal | |||||
noOptionsText={t("No Task List Source")} | |||||
value={taskTemplates.find(taskTemplate => taskTemplate.id === selectedTaskTemplateId)} | |||||
options={[{id: "All", name: t("All tasks")}, ...taskTemplates.map(taskTemplate => ({id: taskTemplate.id, name: taskTemplate.name}))]} | |||||
getOptionLabel={(taskTemplate) => taskTemplate.name} | |||||
isOptionEqualToValue={(option, value) => option?.id === value?.id} | |||||
renderOption={(params, option) => { | |||||
return ( | |||||
<MenuItem {...params} key={option.id} value={option.id}> | |||||
{option.name} | |||||
</MenuItem> | </MenuItem> | ||||
))} | |||||
</Select> | |||||
</FormControl> | |||||
); | |||||
}} | |||||
onChange={onSelectTaskTemplate} | |||||
renderInput={(params) => <TextField {...params} variant="outlined" label={t("Task List Source")} />} | |||||
/> | |||||
</Grid> | </Grid> | ||||
</Grid> | </Grid> | ||||
<TransferList | <TransferList | ||||