| @@ -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 ( | |||
| <Stack spacing={2}> | |||
| <Typography variant="h4">{t("Not Found")}</Typography> | |||
| <Typography variant="body1">{t("The project was not found!")}</Typography> | |||
| <Link href="/projects" component={NextLink} variant="body2"> | |||
| {t("Return to all projects")} | |||
| </Link> | |||
| </Stack> | |||
| ); | |||
| } | |||
| @@ -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<Props> = 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 ( | |||
| <> | |||
| <I18nProvider namespaces={["projects"]}> | |||
| <CreateProject isCopyMode projectId={projectId} /> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default Projects; | |||
| @@ -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 ( | |||
| <Stack spacing={2}> | |||
| <Typography variant="h4">{t("Not Found")}</Typography> | |||
| <Typography variant="body1">{t("The sub project was not found!")}</Typography> | |||
| <Link href="/projects" component={NextLink} variant="body2"> | |||
| {t("Return to all projects")} | |||
| </Link> | |||
| </Stack> | |||
| ); | |||
| } | |||
| @@ -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<Props> = 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 ( | |||
| <> | |||
| <Typography variant="h4">{t("Edit Sub Project")}</Typography> | |||
| <I18nProvider namespaces={["projects"]}> | |||
| <CreateProject isCopyMode isSubProject projectId={projectId}/> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default Projects; | |||
| @@ -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<Props> = ({ | |||
| isEditMode, | |||
| isCopyMode, | |||
| draftId, | |||
| isSubProject, | |||
| mainProjects, | |||
| @@ -546,7 +548,7 @@ const CreateProject: React.FC<Props> = ({ | |||
| } | |||
| }, [totalManhour]); | |||
| const loading = isEditMode ? !Boolean(projectName) : false; | |||
| const loading = isEditMode || isCopyMode ? !Boolean(projectName) : false; | |||
| const submitDisabled = | |||
| loading || | |||
| @@ -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<Props> = async (props) => { | |||
| const [ | |||
| @@ -79,7 +88,7 @@ const CreateProjectWrapper: React.FC<Props> = 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<Props> = 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 ( | |||
| <CreateProject | |||
| isEditMode={props.isEditMode} | |||
| draftId={props.isEditMode ? undefined : props.draftId} | |||
| isEditMode={Boolean(props.isEditMode)} | |||
| isCopyMode={Boolean(props.isCopyMode)} | |||
| draftId={props.isEditMode || props.isCopyMode ? undefined : props.draftId} | |||
| isSubProject={Boolean(props.isSubProject)} | |||
| defaultInputs={projectInfo} | |||
| allTasks={tasks} | |||
| @@ -13,6 +13,7 @@ import { reverse, uniqBy } from "lodash"; | |||
| import { loadDrafts } from "@/app/utils/draftUtils"; | |||
| import { TeamResult } from "@/app/api/team"; | |||
| import { Customer } from "@/app/api/customer"; | |||
| import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | |||
| type ProjectResultOrDraft = ProjectResult & { isDraft?: boolean }; | |||
| @@ -129,6 +130,17 @@ const ProjectSearch: React.FC<Props> = ({ | |||
| [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<Column<ProjectResult>[]>( | |||
| () => [ | |||
| { | |||
| @@ -138,6 +150,16 @@ const ProjectSearch: React.FC<Props> = ({ | |||
| buttonIcon: <EditNote />, | |||
| disabled: !abilities.includes(MAINTAIN_PROJECT), | |||
| }, | |||
| { | |||
| name: "id", | |||
| label: t("Copy"), | |||
| onClick: onProjectCopyClick, | |||
| buttonIcon: <ContentCopyIcon />, | |||
| 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") }, | |||
| @@ -35,6 +35,7 @@ interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | |||
| 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<T extends ResultWithId> = | |||
| @@ -84,6 +85,22 @@ function SearchResults<T extends ResultWithId>({ | |||
| setPage(0); | |||
| }; | |||
| const disabledRows = <T extends ResultWithId> ( | |||
| column: ColumnWithAction<T>, | |||
| 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 = ( | |||
| <> | |||
| <TableContainer sx={{ maxHeight: 440 }}> | |||
| @@ -112,7 +129,7 @@ function SearchResults<T extends ResultWithId>({ | |||
| <IconButton | |||
| color={column.color ?? "primary"} | |||
| onClick={() => column.onClick(item)} | |||
| disabled={Boolean(column.disabled)} | |||
| disabled={Boolean(column.disabled) || Boolean(disabledRows(column, item))} | |||
| > | |||
| {column.buttonIcon} | |||
| </IconButton> | |||