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