From e93c59551d473be8e84da90ce71567ecf086fe96 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 22 May 2024 16:03:15 +0800 Subject: [PATCH] Update project --- .../(main)/projects/create/sub/not-found.tsx | 2 +- src/app/(main)/projects/create/sub/page.tsx | 2 +- .../(main)/projects/edit/sub/not-found.tsx | 17 +++++ src/app/(main)/projects/edit/sub/page.tsx | 76 +++++++++++++++++++ src/app/api/projects/actions.ts | 4 +- src/app/api/projects/index.ts | 1 + .../ControlledAutoComplete.tsx | 29 ++++--- .../CreateProject/CreateProject.tsx | 1 + src/components/CreateProject/Milestone.tsx | 42 +++++----- .../CreateProject/ProjectClientDetails.tsx | 18 +++-- .../CreateProject/StaffAllocation.tsx | 37 +++++---- src/components/CreateProject/TaskSetup.tsx | 6 +- .../ProjectSearch/ProjectSearch.tsx | 5 +- 13 files changed, 182 insertions(+), 58 deletions(-) create mode 100644 src/app/(main)/projects/edit/sub/not-found.tsx create mode 100644 src/app/(main)/projects/edit/sub/page.tsx diff --git a/src/app/(main)/projects/create/sub/not-found.tsx b/src/app/(main)/projects/create/sub/not-found.tsx index 1cc4df3..9b28f5d 100644 --- a/src/app/(main)/projects/create/sub/not-found.tsx +++ b/src/app/(main)/projects/create/sub/not-found.tsx @@ -8,7 +8,7 @@ export default async function NotFound() { return ( {t("Not Found")} - {t("The sub project was not found or there was no any main projects!")} + {t("There was no any main projects!")} {t("Return to all projects")} diff --git a/src/app/(main)/projects/create/sub/page.tsx b/src/app/(main)/projects/create/sub/page.tsx index 1a8fab7..3f474de 100644 --- a/src/app/(main)/projects/create/sub/page.tsx +++ b/src/app/(main)/projects/create/sub/page.tsx @@ -21,7 +21,7 @@ import { Metadata } from "next"; import { notFound } from "next/navigation"; export const metadata: Metadata = { - title: "Create Project", + title: "Create Sub Project", }; const Projects: React.FC = async () => { diff --git a/src/app/(main)/projects/edit/sub/not-found.tsx b/src/app/(main)/projects/edit/sub/not-found.tsx new file mode 100644 index 0000000..234e436 --- /dev/null +++ b/src/app/(main)/projects/edit/sub/not-found.tsx @@ -0,0 +1,17 @@ +import { getServerI18n } from "@/i18n"; +import { Stack, Typography, Link } from "@mui/material"; +import NextLink from "next/link"; + +export default async function NotFound() { + const { t } = await getServerI18n("projects", "common"); + + return ( + + {t("Not Found")} + {t("The sub project was not found!")} + + {t("Return to all projects")} + + + ); +} diff --git a/src/app/(main)/projects/edit/sub/page.tsx b/src/app/(main)/projects/edit/sub/page.tsx new file mode 100644 index 0000000..eb4f5c6 --- /dev/null +++ b/src/app/(main)/projects/edit/sub/page.tsx @@ -0,0 +1,76 @@ +import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; +import { fetchGrades } from "@/app/api/grades"; +import { + fetchMainProjects, + fetchProjectBuildingTypes, + fetchProjectCategories, + fetchProjectContractTypes, + fetchProjectDetails, + fetchProjectFundingTypes, + fetchProjectLocationTypes, + fetchProjectServiceTypes, + fetchProjectWorkNatures, +} from "@/app/api/projects"; +import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; +import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; +import CreateProject from "@/components/CreateProject"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { isArray } from "lodash"; +import { Metadata } from "next"; +import { notFound } from "next/navigation"; + +interface Props { + searchParams: { [key: string]: string | string[] | undefined }; +} + +export const metadata: Metadata = { + title: "Edit Sub Project", +}; + +const Projects: React.FC = async ({ searchParams }) => { + const { t } = await getServerI18n("projects"); + const projectId = searchParams["id"]; + + if (!projectId || isArray(projectId)) { + notFound(); + } + + // Preload necessary dependencies + fetchAllTasks(); + fetchTaskTemplates(); + fetchProjectCategories(); + fetchProjectContractTypes(); + fetchProjectFundingTypes(); + fetchProjectLocationTypes(); + fetchProjectServiceTypes(); + fetchProjectBuildingTypes(); + fetchProjectWorkNatures(); + fetchAllCustomers(); + fetchAllSubsidiaries(); + fetchGrades(); + preloadTeamLeads(); + preloadStaff(); + + try { + await fetchProjectDetails(projectId); + const data = await fetchMainProjects(); + + if (!Boolean(data) || data.length === 0) { + notFound(); + } + } catch (e) { + notFound(); + } + + return ( + <> + {t("Edit Sub Project")} + + + + + ); +}; + +export default Projects; diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts index 97cb34e..31ab2c9 100644 --- a/src/app/api/projects/actions.ts +++ b/src/app/api/projects/actions.ts @@ -36,8 +36,8 @@ export interface CreateProjectInputs { // Client details clientId: Customer["id"]; clientContactId?: number; - clientSubsidiaryId?: number; - subsidiaryContactId: number; + clientSubsidiaryId?: number | null; + subsidiaryContactId?: number; isSubsidiaryContact?: boolean; // Allocation diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index d25ba16..6833571 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -13,6 +13,7 @@ export interface ProjectResult { team: string; client: string; status: string; + mainProject: string; } export interface MainProject { diff --git a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx index e37f348..36e7542 100644 --- a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx +++ b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx @@ -1,6 +1,6 @@ "use client" -import { Autocomplete, MenuItem, TextField, Checkbox } from "@mui/material"; +import { Autocomplete, MenuItem, TextField, Checkbox, Chip } from "@mui/material"; import { Controller, FieldValues, Path, Control, RegisterOptions } from "react-hook-form"; import { useTranslation } from "react-i18next"; import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; @@ -18,6 +18,7 @@ interface Props + disabled?: boolean, } function ControlledAutoComplete< @@ -27,11 +28,10 @@ function ControlledAutoComplete< props: Props ) { const { t } = useTranslation() - const { control, options, name, label, noOptionsText, isMultiple, rules } = props; + const { control, options, name, label, noOptionsText, isMultiple, rules, disabled } = 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] = [] @@ -42,7 +42,6 @@ function ControlledAutoComplete< name={name} control={control} rules={rules} - render={({ field, fieldState, formState }) => { return ( @@ -51,7 +50,8 @@ function ControlledAutoComplete< multiple disableClearable disableCloseOnSelect - disablePortal + // disablePortal + disabled={disabled} noOptionsText={noOptionsText ?? t("No Options")} value={options.filter(option => { return field.value?.includes(option.id) @@ -61,7 +61,7 @@ function ControlledAutoComplete< isOptionEqualToValue={(option, value) => option.id === value.id} renderOption={(params, option, { selected }) => { return ( -
  • +
  • ); }} + // renderTags={(tagValue, getTagProps) => { + // return tagValue.map((option, index) => ( + // + // )) + // }} onChange={(event, value) => { field.onChange(value?.map(v => v.id)) }} @@ -80,7 +85,8 @@ function ControlledAutoComplete< : option.id === field.value) ?? options[0]} options={options} @@ -88,13 +94,18 @@ function ControlledAutoComplete< isOptionEqualToValue={(option, value) => option?.id === value?.id} renderOption={(params, option) => { return ( - + {option.label ?? option.name} ); }} + // renderTags={(tagValue, getTagProps) => { + // return tagValue.map((option, index) => ( + // + // )) + // }} onChange={(event, value) => { - field.onChange(value?.id) + field.onChange(value?.id ?? null) }} renderInput={(params) => } />) diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 5f35919..d115f3a 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -393,6 +393,7 @@ const CreateProject: React.FC = ({ projectCategories={projectCategories} teamLeads={teamLeads} isActive={tabIndex === 0} + isEditMode={isEditMode} /> } { diff --git a/src/components/CreateProject/Milestone.tsx b/src/components/CreateProject/Milestone.tsx index c91c0fa..f2e62ce 100644 --- a/src/components/CreateProject/Milestone.tsx +++ b/src/components/CreateProject/Milestone.tsx @@ -4,16 +4,18 @@ import Card from "@mui/material/Card"; import CardContent from "@mui/material/CardContent"; import { useTranslation } from "react-i18next"; import Button from "@mui/material/Button"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { SyntheticEvent, useCallback, useEffect, useMemo, useState } from "react"; import CardActions from "@mui/material/CardActions"; import RestartAlt from "@mui/icons-material/RestartAlt"; import { Alert, + Autocomplete, FormControl, InputLabel, MenuItem, Select, SelectChangeEvent, + TextField, } from "@mui/material"; import { Task, TaskGroup } from "@/app/api/tasks"; import uniqBy from "lodash/uniqBy"; @@ -49,8 +51,8 @@ const Milestone: React.FC = ({ allTasks, isActive }) => { taskGroups[0].id, ); const onSelectTaskGroup = useCallback( - (event: SelectChangeEvent) => { - const id = event.target.value; + (event: SyntheticEvent, value: NonNullable) => { + const id = value.id; const newTaksGroupId = typeof id === "string" ? parseInt(id) : id; setCurrentTaskGroupId(newTaksGroupId); }, @@ -81,7 +83,7 @@ const Milestone: React.FC = ({ allTasks, isActive }) => { } // console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0)) if (hasError) { - setError("milestones", {message: "milestones is not valid", type: "invalid"}) + setError("milestones", { message: "milestones is not valid", type: "invalid" }) } else { clearErrors("milestones") } @@ -92,26 +94,32 @@ const Milestone: React.FC = ({ allTasks, isActive }) => { - {t("Task Stage")} - + renderInput={(params) => } + /> {/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */} {isActive && } - + {/* - + */} diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index 8fb7fab..9e4ac7f 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -39,6 +39,7 @@ import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComp interface Props { isActive: boolean; isSubProject: boolean; + isEditMode: boolean; mainProjects?: MainProject[]; projectCategories: ProjectCategory[]; teamLeads: StaffResult[]; @@ -55,6 +56,7 @@ interface Props { const ProjectClientDetails: React.FC = ({ isActive, isSubProject, + isEditMode, mainProjects, projectCategories, teamLeads, @@ -110,6 +112,7 @@ const ProjectClientDetails: React.FC = ({ ); // get customer (client) contact combo + const [firstCustomerLoaded, setFirstCustomerLoaded] = useState(false) useEffect(() => { if (selectedCustomerId !== undefined) { fetchCustomer(selectedCustomerId).then(({ contacts, subsidiaryIds }) => { @@ -118,7 +121,7 @@ const ProjectClientDetails: React.FC = ({ // if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0]) // else - setValue("clientSubsidiaryId", undefined) + if (isEditMode && !firstCustomerLoaded) { setFirstCustomerLoaded(true) } else setValue("clientSubsidiaryId", null) // if (contacts.length > 0) setValue("clientContactId", contacts[0].id) // else setValue("clientContactId", undefined) }); @@ -130,11 +133,11 @@ const ProjectClientDetails: React.FC = ({ if (Boolean(clientSubsidiaryId)) { // get subsidiary contact combo const contacts = allSubsidiaries.find(subsidiary => subsidiary.id === clientSubsidiaryId)?.subsidiaryContacts!! - setSubsidiaryContacts(contacts) + 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([]) + 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) } @@ -153,7 +156,7 @@ const ProjectClientDetails: React.FC = ({ // Automatically update the project & client details whene select a main project const mainProjectId = watch("mainProjectId") useEffect(() => { - if (mainProjectId !== undefined && mainProjects !== undefined) { + if (mainProjectId !== undefined && mainProjects !== undefined && !isEditMode) { const mainProject = mainProjects.find(project => project.projectId === mainProjectId); if (mainProject !== undefined) { @@ -174,7 +177,7 @@ const ProjectClientDetails: React.FC = ({ setValue("clientContactId", mainProject.clientContactId) } } - }, [getValues, mainProjectId, setValue]) + }, [getValues, mainProjectId, setValue, isEditMode]) // const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>( // (acc, building) => ({ ...acc, [building.id]: building.name }), @@ -202,6 +205,7 @@ const ProjectClientDetails: React.FC = ({ name="mainProjectId" label={t("Main Project")} noOptionsText={t("No Main Project")} + disabled={isEditMode} /> @@ -438,11 +442,11 @@ const ProjectClientDetails: React.FC = ({ )} - + {/* - + */} ); diff --git a/src/components/CreateProject/StaffAllocation.tsx b/src/components/CreateProject/StaffAllocation.tsx index 81d3c97..e61f995 100644 --- a/src/components/CreateProject/StaffAllocation.tsx +++ b/src/components/CreateProject/StaffAllocation.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslation } from "react-i18next"; -import React, { useEffect, useMemo } from "react"; +import React, { SyntheticEvent, useEffect, useMemo } from "react"; import RestartAlt from "@mui/icons-material/RestartAlt"; import SearchResults, { Column } from "../SearchResults"; import { Search, Clear, PersonAdd, PersonRemove } from "@mui/icons-material"; @@ -160,8 +160,8 @@ const StaffAllocation: React.FC = ({ }, [columnFilters]); const [filters, setFilters] = React.useState(defaultFilterValues); const makeFilterSelect = React.useCallback( - (filter: keyof StaffResult) => (event: SelectChangeEvent) => { - setFilters((f) => ({ ...f, [filter]: event.target.value })); + (filter: keyof StaffResult) => (event: SyntheticEvent, value: NonNullable) => { + setFilters((f) => ({ ...f, [filter]: value })); }, [], ); @@ -239,20 +239,25 @@ const StaffAllocation: React.FC = ({ return ( - {label} - + renderInput={(params) => } + /> ); @@ -289,11 +294,11 @@ const StaffAllocation: React.FC = ({ )} - + {/* - + */} {/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */} diff --git a/src/components/CreateProject/TaskSetup.tsx b/src/components/CreateProject/TaskSetup.tsx index fe02f37..759b05f 100644 --- a/src/components/CreateProject/TaskSetup.tsx +++ b/src/components/CreateProject/TaskSetup.tsx @@ -135,7 +135,7 @@ const TaskSetup: React.FC = ({ taskTemplate.id === selectedTaskTemplateId)} options={[{id: "All", name: t("All tasks")}, ...taskTemplates.map(taskTemplate => ({id: taskTemplate.id, name: taskTemplate.name}))]} @@ -207,11 +207,11 @@ const TaskSetup: React.FC = ({ allItemsLabel={t("Task Pool")} selectedItemsLabel={t("Project Task List")} /> - + {/* - + */} ); diff --git a/src/components/ProjectSearch/ProjectSearch.tsx b/src/components/ProjectSearch/ProjectSearch.tsx index 79ee51b..bbb1d67 100644 --- a/src/components/ProjectSearch/ProjectSearch.tsx +++ b/src/components/ProjectSearch/ProjectSearch.tsx @@ -20,7 +20,6 @@ type SearchParamNames = keyof SearchQuery; const ProjectSearch: React.FC = ({ projects, projectCategories }) => { const router = useRouter(); const { t } = useTranslation("projects"); - console.log(projects) const [filteredProjects, setFilteredProjects] = useState(projects); @@ -62,7 +61,9 @@ const ProjectSearch: React.FC = ({ projects, projectCategories }) => { const onProjectClick = useCallback( (project: ProjectResult) => { - router.push(`/projects/edit?id=${project.id}`); + if (Boolean(project.mainProject)) { + router.push(`/projects/edit/sub?id=${project.id}`); + } else router.push(`/projects/edit?id=${project.id}`); }, [router], );