From 7ba7f838c9ddc1b83dc456460533b6e203aca619 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 23 Jan 2025 17:40:30 +0800 Subject: [PATCH] Add copy project function --- src/app/(main)/projects/copy/not-found.tsx | 17 ++++ src/app/(main)/projects/copy/page.tsx | 77 ++++++++++++++++++ src/app/(main)/projects/copySub/not-found.tsx | 17 ++++ src/app/(main)/projects/copySub/page.tsx | 79 +++++++++++++++++++ .../CreateProject/CreateProject.tsx | 4 +- .../CreateProject/CreateProjectWrapper.tsx | 34 ++++++-- .../ProjectSearch/ProjectSearch.tsx | 22 ++++++ .../SearchResults/SearchResults.tsx | 19 ++++- 8 files changed, 262 insertions(+), 7 deletions(-) create mode 100644 src/app/(main)/projects/copy/not-found.tsx create mode 100644 src/app/(main)/projects/copy/page.tsx create mode 100644 src/app/(main)/projects/copySub/not-found.tsx create mode 100644 src/app/(main)/projects/copySub/page.tsx diff --git a/src/app/(main)/projects/copy/not-found.tsx b/src/app/(main)/projects/copy/not-found.tsx new file mode 100644 index 0000000..14e0e6d --- /dev/null +++ b/src/app/(main)/projects/copy/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 project was not found!")} + + {t("Return to all projects")} + + + ); +} diff --git a/src/app/(main)/projects/copy/page.tsx b/src/app/(main)/projects/copy/page.tsx new file mode 100644 index 0000000..8ff2777 --- /dev/null +++ b/src/app/(main)/projects/copy/page.tsx @@ -0,0 +1,77 @@ +import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; +import { fetchGrades } from "@/app/api/grades"; +import { + 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 { fetchUserAbilities } from "@/app/utils/fetchUtil"; +import { ServerFetchError } from "@/app/utils/fetchUtil"; +import CreateProject from "@/components/CreateProject"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import { MAINTAIN_PROJECT } from "@/middleware"; +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: "Copy Project", +}; + +const Projects: React.FC = async ({ searchParams }) => { + const { t } = await getServerI18n("projects"); + // Assume projectId is string here + const projectId = searchParams["id"]; + const abilities = await fetchUserAbilities() + + if (!projectId || isArray(projectId) || ![MAINTAIN_PROJECT].some(ability => abilities.includes(ability))) { + notFound(); + } + + // Preload necessary dependencies + fetchAllTasks(); + fetchTaskTemplates(); + fetchProjectCategories(); + fetchProjectContractTypes(); + fetchProjectFundingTypes(); + fetchProjectLocationTypes(); + fetchProjectServiceTypes(); + fetchProjectBuildingTypes(); + fetchProjectWorkNatures(); + fetchAllCustomers(); + fetchAllSubsidiaries(); + fetchGrades(); + preloadTeamLeads(); + preloadStaff(); + + try { + console.log(projectId) + await fetchProjectDetails(projectId); + } catch (e) { + if (e instanceof ServerFetchError && e.response?.status === 404) { + notFound(); + } + } + + return ( + <> + + + + + ); +}; + +export default Projects; diff --git a/src/app/(main)/projects/copySub/not-found.tsx b/src/app/(main)/projects/copySub/not-found.tsx new file mode 100644 index 0000000..234e436 --- /dev/null +++ b/src/app/(main)/projects/copySub/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/copySub/page.tsx b/src/app/(main)/projects/copySub/page.tsx new file mode 100644 index 0000000..13b37da --- /dev/null +++ b/src/app/(main)/projects/copySub/page.tsx @@ -0,0 +1,79 @@ +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 { fetchUserAbilities } from "@/app/utils/fetchUtil"; +import CreateProject from "@/components/CreateProject"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import { MAINTAIN_PROJECT } from "@/middleware"; +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"]; + + const abilities = await fetchUserAbilities() + if (!projectId || isArray(projectId) || !abilities.includes(MAINTAIN_PROJECT)) { + 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/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 444e22e..f4452a9 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -64,6 +64,7 @@ import { deleteDraft, loadDraft, saveToLocalStorage } from "@/app/utils/draftUti export interface Props { isEditMode: boolean; + isCopyMode: boolean; draftId?: number; isSubProject: boolean; mainProjects?: MainProject[]; @@ -116,6 +117,7 @@ const hasErrorsInTab = ( const CreateProject: React.FC = ({ isEditMode, + isCopyMode, draftId, isSubProject, mainProjects, @@ -546,7 +548,7 @@ const CreateProject: React.FC = ({ } }, [totalManhour]); - const loading = isEditMode ? !Boolean(projectName) : false; + const loading = isEditMode || isCopyMode ? !Boolean(projectName) : false; const submitDisabled = loading || diff --git a/src/components/CreateProject/CreateProjectWrapper.tsx b/src/components/CreateProject/CreateProjectWrapper.tsx index 34c4f07..382a85d 100644 --- a/src/components/CreateProject/CreateProjectWrapper.tsx +++ b/src/components/CreateProject/CreateProjectWrapper.tsx @@ -21,17 +21,26 @@ import { fetchGrades } from "@/app/api/grades"; import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; type CreateProjectProps = { - isEditMode: false; + isEditMode?: false; + isCopyMode?: false; isSubProject?: boolean; draftId?: number; }; interface EditProjectProps { isEditMode: true; + isCopyMode?: false; projectId?: string; isSubProject?: boolean; } -type Props = CreateProjectProps | EditProjectProps; +interface CopyProjectProps { + isEditMode?: false; + isCopyMode: true; + projectId?: string; + isSubProject?: boolean; +} + +type Props = CreateProjectProps | EditProjectProps | CopyProjectProps; const CreateProjectWrapper: React.FC = async (props) => { const [ @@ -79,7 +88,7 @@ const CreateProjectWrapper: React.FC = async (props) => { (teamLead) => teamLead.teamId === teamId || teamLead.team == "ST", ) } - const projectInfo = props.isEditMode + const projectInfo = props.isEditMode || props.isCopyMode ? await fetchProjectDetails(props.projectId!) : undefined; @@ -87,10 +96,25 @@ const CreateProjectWrapper: React.FC = async (props) => { ? await fetchMainProjects() : undefined; + if (props.isCopyMode && projectInfo) { + projectInfo.projectId = null + projectInfo.projectCode = projectInfo.projectCode + "-copy" + projectInfo.projectName = projectInfo.projectName + "-copy" + projectInfo.projectStatus = "" + Object.entries(projectInfo.milestones).forEach(([key, value]) => { + projectInfo.milestones[Number(key)].payments.forEach(({ ...rest}, idx, orig) => { + orig[idx] = { ...rest, id: rest.id * -1 } + }) + + // console.log(projectInfo.milestones[Number(key)].payments) + }) + } + return ( = ({ [router], ); + const onProjectCopyClick = useCallback( + (project: ProjectResultOrDraft) => { + if (!project.isDraft) { + if (Boolean(project.mainProject)) { + router.push(`/projects/copySub?id=${project.id}`); + } else router.push(`/projects/copy?id=${project.id}`); + } + }, + [router], + ); + const columns = useMemo[]>( () => [ { @@ -138,6 +150,16 @@ const ProjectSearch: React.FC = ({ buttonIcon: , disabled: !abilities.includes(MAINTAIN_PROJECT), }, + { + name: "id", + label: t("Copy"), + onClick: onProjectCopyClick, + buttonIcon: , + disabled: !abilities.includes(MAINTAIN_PROJECT), + disabledRows: { + status: ["Draft"] + } + }, { name: "code", label: t("Project Code") }, { name: "name", label: t("Project Name") }, { name: "category", label: t("Project Category") }, diff --git a/src/components/SearchResults/SearchResults.tsx b/src/components/SearchResults/SearchResults.tsx index 05bdf1c..2fe6bf3 100644 --- a/src/components/SearchResults/SearchResults.tsx +++ b/src/components/SearchResults/SearchResults.tsx @@ -35,6 +35,7 @@ interface ColumnWithAction extends BaseColumn { onClick: (item: T) => void; buttonIcon: React.ReactNode; disabled?: boolean; + disabledRows?: { [columnName in keyof T]: string[] }; // Filter the row which is going to be disabled } export type Column = @@ -84,6 +85,22 @@ function SearchResults({ setPage(0); }; + const disabledRows = ( + column: ColumnWithAction, + item: T + ): Boolean => { + if (column.disabledRows) { + for (const [key, value] of Object.entries(column.disabledRows)) { + if (value + .map(v => v.toLowerCase()) + .includes(String(item[key as keyof T]).toLowerCase()) + ) return true; + } + } + + return false; + }; + const table = ( <> @@ -112,7 +129,7 @@ function SearchResults({ column.onClick(item)} - disabled={Boolean(column.disabled)} + disabled={Boolean(column.disabled) || Boolean(disabledRows(column, item))} > {column.buttonIcon}