From 1ab7d7223a5d76fbac4b3161b70c64482dd1e166 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 21 May 2024 18:41:07 +0800 Subject: [PATCH] add "create sup project" --- src/app/(main)/projects/create/sub/page.tsx | 14 +- src/app/api/projects/actions.ts | 1 + src/app/api/projects/index.ts | 23 +++- src/components/Breadcrumb/Breadcrumb.tsx | 2 + .../ControlledAutoComplete.tsx | 125 ++++++++++-------- .../CreateProject/CreateProject.tsx | 18 +-- .../CreateProject/CreateProjectWrapper.tsx | 13 +- .../CreateProject/ProjectClientDetails.tsx | 52 +++++++- .../CreateProject/StaffAllocation.tsx | 1 + src/components/CreateProject/TaskSetup.tsx | 40 +++--- 10 files changed, 197 insertions(+), 92 deletions(-) diff --git a/src/app/(main)/projects/create/sub/page.tsx b/src/app/(main)/projects/create/sub/page.tsx index bd5837e..1a8fab7 100644 --- a/src/app/(main)/projects/create/sub/page.tsx +++ b/src/app/(main)/projects/create/sub/page.tsx @@ -1,6 +1,7 @@ import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; import { fetchGrades } from "@/app/api/grades"; import { + fetchMainProjects, fetchProjectBuildingTypes, fetchProjectCategories, fetchProjectContractTypes, @@ -12,10 +13,12 @@ import { } from "@/app/api/projects"; import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; +import { ServerFetchError } from "@/app/utils/fetchUtil"; import CreateProject from "@/components/CreateProject"; import { I18nProvider, getServerI18n } from "@/i18n"; import Typography from "@mui/material/Typography"; import { Metadata } from "next"; +import { notFound } from "next/navigation"; export const metadata: Metadata = { title: "Create Project", @@ -39,12 +42,21 @@ const Projects: React.FC = async () => { fetchGrades(); preloadTeamLeads(); preloadStaff(); + try { + const data = await fetchMainProjects(); + + if (!Boolean(data) || data.length === 0) { + notFound(); + } + } catch (e) { + notFound(); + } return ( <> {t("Create Sub Project")} - + ); diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts index c1be476..97cb34e 100644 --- a/src/app/api/projects/actions.ts +++ b/src/app/api/projects/actions.ts @@ -22,6 +22,7 @@ export interface CreateProjectInputs { projectActualEnd: string; projectStatus: string; isClpProject: boolean; + mainProjectId?: number | null; // Project info serviceTypeId: number; diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index 7a64ea1..d25ba16 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -15,6 +15,27 @@ export interface ProjectResult { 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 { id: number; name: string; @@ -82,7 +103,7 @@ export const fetchProjects = cache(async () => { }); export const fetchMainProjects = cache(async () => { - return serverFetchJson(`${BASE_API_URL}/projects/main`, { + return serverFetchJson(`${BASE_API_URL}/projects/main`, { next: { tags: ["projects"] }, }); }); diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 516aca2..8c1b9b0 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -15,7 +15,9 @@ const pathToLabelMap: { [path: string]: string } = { "/home": "User Workspace", "/projects": "Projects", "/projects/create": "Create Project", + "/projects/create/sub": "Sub Project", "/projects/edit": "Edit Project", + "/projects/edit/sub": "Sub Project", "/tasks": "Task Template", "/tasks/create": "Create Task Template", "/staffReimbursement": "Staff Reimbursement", diff --git a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx index d8a2962..e37f348 100644 --- a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx +++ b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx @@ -10,7 +10,7 @@ const icon = ; const checkedIcon = ; // label -> e.g. code - name -> 001 - WL // name -> WL -interface Props { +interface Props { control: Control, options: T[], name: Path, // register name @@ -18,7 +18,6 @@ interface Props - error?: boolean, } function ControlledAutoComplete< @@ -28,68 +27,78 @@ function ControlledAutoComplete< props: Props ) { 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 ( ( - isMultiple ? - { - // 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 ( -
  • - - {option.label ?? option.name} -
  • - ); - }} - onChange={(event, value) => { - field.onChange(value?.map(v => v.id)) - }} - renderInput={(params) => } - /> - : - 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 ( - - {option.label ?? option.name} - - ); - }} - onChange={(event, value) => { - field.onChange(value?.id) - }} - renderInput={(params) => } - /> - )} + + render={({ field, fieldState, formState }) => { + + return ( + isMultiple ? + { + 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 ( +
  • + + {option.label ?? option.name} +
  • + ); + }} + onChange={(event, value) => { + field.onChange(value?.map(v => v.id)) + }} + renderInput={(params) => } + /> + : + 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 ( + + {option.label ?? option.name} + + ); + }} + onChange={(event, value) => { + field.onChange(value?.id) + }} + renderInput={(params) => } + />) + }} /> ) } diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 937bac5..5f35919 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -33,6 +33,7 @@ import { ContractType, FundingType, LocationType, + MainProject, ProjectCategory, ServiceType, WorkNature, @@ -52,6 +53,8 @@ import dayjs from "dayjs"; export interface Props { isEditMode: boolean; + isSubProject: boolean; + mainProjects?: MainProject[]; defaultInputs?: CreateProjectInputs; allTasks: Task[]; projectCategories: ProjectCategory[]; @@ -93,6 +96,8 @@ const hasErrorsInTab = ( const CreateProject: React.FC = ({ isEditMode, + isSubProject, + mainProjects, defaultInputs, allTasks, projectCategories, @@ -269,14 +274,9 @@ const CreateProject: React.FC = ({ milestones: {}, totalManhour: 0, 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, // manhourPercentageByGrade should have a sensible default @@ -380,6 +380,8 @@ const CreateProject: React.FC = ({ { = async (props) => { ? await fetchProjectDetails(props.projectId!) : undefined; + const mainProjects = Boolean(props.isSubProject) + ? await fetchMainProjects() + : undefined; + return ( = async (props) => { workNatures={workNatures} allStaffs={allStaffs} grades={grades} + mainProjects={mainProjects} /> ); }; diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index d77defe..8fb7fab 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -22,6 +22,7 @@ import { ContractType, FundingType, LocationType, + MainProject, ProjectCategory, ServiceType, WorkNature, @@ -37,6 +38,8 @@ import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComp interface Props { isActive: boolean; + isSubProject: boolean; + mainProjects?: MainProject[]; projectCategories: ProjectCategory[]; teamLeads: StaffResult[]; allCustomers: Customer[]; @@ -51,6 +54,8 @@ interface Props { const ProjectClientDetails: React.FC = ({ isActive, + isSubProject, + mainProjects, projectCategories, teamLeads, allCustomers, @@ -70,6 +75,8 @@ const ProjectClientDetails: React.FC = ({ control, setValue, getValues, + reset, + resetField, } = useFormContext(); const subsidiaryMap = useMemo<{ @@ -136,7 +143,6 @@ const ProjectClientDetails: React.FC = ({ // Automatically add the team lead to the allocated staff list const selectedTeamLeadId = watch("projectLeadId"); useEffect(() => { - console.log(selectedTeamLeadId) if (selectedTeamLeadId !== undefined) { const currentStaffIds = getValues("allocatedStaffIds"); const newList = uniq([...currentStaffIds, selectedTeamLeadId]); @@ -144,6 +150,32 @@ const ProjectClientDetails: React.FC = ({ } }, [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 }>( // (acc, building) => ({ ...acc, [building.id]: building.name }), // {}, @@ -162,6 +194,18 @@ const ProjectClientDetails: React.FC = ({ {t("Project Details")} + { + isSubProject && mainProjects !== undefined && <> + ({ id: mainProject.projectId, label: `${mainProject.projectCode} - ${mainProject.projectName}` }))]} + name="mainProjectId" + label={t("Main Project")} + noOptionsText={t("No Main Project")} + /> + + + } = ({ {t("CLP Project")} @@ -317,7 +361,6 @@ const ProjectClientDetails: React.FC = ({ rules={{ required: "Please select a client" }} - error={Boolean(errors.clientId)} /> @@ -337,7 +380,7 @@ const ProjectClientDetails: React.FC = ({ subsidiaryMap[subId]) .map((subsidiaryId, index) => { const subsidiary = subsidiaryMap[subsidiaryId] @@ -368,7 +411,6 @@ const ProjectClientDetails: React.FC = ({ } else return true; }, }} - error={Boolean(errors.clientContactId)} /> diff --git a/src/components/CreateProject/StaffAllocation.tsx b/src/components/CreateProject/StaffAllocation.tsx index bd699ec..81d3c97 100644 --- a/src/components/CreateProject/StaffAllocation.tsx +++ b/src/components/CreateProject/StaffAllocation.tsx @@ -25,6 +25,7 @@ import { Tab, Tabs, SelectChangeEvent, + Autocomplete, } from "@mui/material"; import differenceWith from "lodash/differenceWith"; import intersectionWith from "lodash/intersectionWith"; diff --git a/src/components/CreateProject/TaskSetup.tsx b/src/components/CreateProject/TaskSetup.tsx index dac0698..fe02f37 100644 --- a/src/components/CreateProject/TaskSetup.tsx +++ b/src/components/CreateProject/TaskSetup.tsx @@ -7,7 +7,7 @@ import Typography from "@mui/material/Typography"; import { useTranslation } from "react-i18next"; import TransferList from "../TransferList"; 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 RestartAlt from "@mui/icons-material/RestartAlt"; import FormControl from "@mui/material/FormControl"; @@ -20,6 +20,7 @@ import { CreateProjectInputs, ManhourAllocation } from "@/app/api/projects/actio import isNumber from "lodash/isNumber"; import intersectionWith from "lodash/intersectionWith"; import { difference } from "lodash"; +import { Autocomplete, TextField } from "@mui/material"; interface Props { allTasks: Task[]; @@ -50,9 +51,9 @@ const TaskSetup: React.FC = ({ "All" | number >(watch("taskTemplateId") ?? "All"); const onSelectTaskTemplate = useCallback( - (e: SelectChangeEvent) => { - if (e.target.value === "All" || isNumber(e.target.value)) { - setSelectedTaskTemplateId(e.target.value); + (event: SyntheticEvent, value: NonNullable<{id: number | string, name: string}>) => { + if (value.id === "All" || isNumber(value.id)) { + setSelectedTaskTemplateId(value.id); // onReset(); } }, @@ -132,21 +133,24 @@ const TaskSetup: React.FC = ({ marginBlockEnd={1} > - - {t("Task List Source")} - - label={t("Task List Source")} - value={selectedTaskTemplateId} - onChange={onSelectTaskTemplate} - > - {t("All tasks")} - {taskTemplates.map((template, index) => ( - - {template.name} + 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 ( + + {option.name} - ))} - - + ); + }} + onChange={onSelectTaskTemplate} + renderInput={(params) => } + />