@@ -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 { | export interface Props { | ||||
isEditMode: boolean; | isEditMode: boolean; | ||||
isCopyMode: boolean; | |||||
draftId?: number; | draftId?: number; | ||||
isSubProject: boolean; | isSubProject: boolean; | ||||
mainProjects?: MainProject[]; | mainProjects?: MainProject[]; | ||||
@@ -116,6 +117,7 @@ const hasErrorsInTab = ( | |||||
const CreateProject: React.FC<Props> = ({ | const CreateProject: React.FC<Props> = ({ | ||||
isEditMode, | isEditMode, | ||||
isCopyMode, | |||||
draftId, | draftId, | ||||
isSubProject, | isSubProject, | ||||
mainProjects, | mainProjects, | ||||
@@ -546,7 +548,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
} | } | ||||
}, [totalManhour]); | }, [totalManhour]); | ||||
const loading = isEditMode ? !Boolean(projectName) : false; | |||||
const loading = isEditMode || isCopyMode ? !Boolean(projectName) : false; | |||||
const submitDisabled = | const submitDisabled = | ||||
loading || | loading || | ||||
@@ -21,17 +21,26 @@ import { fetchGrades } from "@/app/api/grades"; | |||||
import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | ||||
type CreateProjectProps = { | type CreateProjectProps = { | ||||
isEditMode: false; | |||||
isEditMode?: false; | |||||
isCopyMode?: false; | |||||
isSubProject?: boolean; | isSubProject?: boolean; | ||||
draftId?: number; | draftId?: number; | ||||
}; | }; | ||||
interface EditProjectProps { | interface EditProjectProps { | ||||
isEditMode: true; | isEditMode: true; | ||||
isCopyMode?: false; | |||||
projectId?: string; | projectId?: string; | ||||
isSubProject?: boolean; | 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 CreateProjectWrapper: React.FC<Props> = async (props) => { | ||||
const [ | const [ | ||||
@@ -79,7 +88,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
(teamLead) => teamLead.teamId === teamId || teamLead.team == "ST", | (teamLead) => teamLead.teamId === teamId || teamLead.team == "ST", | ||||
) | ) | ||||
} | } | ||||
const projectInfo = props.isEditMode | |||||
const projectInfo = props.isEditMode || props.isCopyMode | |||||
? await fetchProjectDetails(props.projectId!) | ? await fetchProjectDetails(props.projectId!) | ||||
: undefined; | : undefined; | ||||
@@ -87,10 +96,25 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
? await fetchMainProjects() | ? await fetchMainProjects() | ||||
: undefined; | : 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 ( | return ( | ||||
<CreateProject | <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)} | isSubProject={Boolean(props.isSubProject)} | ||||
defaultInputs={projectInfo} | defaultInputs={projectInfo} | ||||
allTasks={tasks} | allTasks={tasks} | ||||
@@ -13,6 +13,7 @@ import { reverse, uniqBy } from "lodash"; | |||||
import { loadDrafts } from "@/app/utils/draftUtils"; | import { loadDrafts } from "@/app/utils/draftUtils"; | ||||
import { TeamResult } from "@/app/api/team"; | import { TeamResult } from "@/app/api/team"; | ||||
import { Customer } from "@/app/api/customer"; | import { Customer } from "@/app/api/customer"; | ||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | |||||
type ProjectResultOrDraft = ProjectResult & { isDraft?: boolean }; | type ProjectResultOrDraft = ProjectResult & { isDraft?: boolean }; | ||||
@@ -129,6 +130,17 @@ const ProjectSearch: React.FC<Props> = ({ | |||||
[router], | [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>[]>( | const columns = useMemo<Column<ProjectResult>[]>( | ||||
() => [ | () => [ | ||||
{ | { | ||||
@@ -138,6 +150,16 @@ const ProjectSearch: React.FC<Props> = ({ | |||||
buttonIcon: <EditNote />, | buttonIcon: <EditNote />, | ||||
disabled: !abilities.includes(MAINTAIN_PROJECT), | 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: "code", label: t("Project Code") }, | ||||
{ name: "name", label: t("Project Name") }, | { name: "name", label: t("Project Name") }, | ||||
{ name: "category", label: t("Project Category") }, | { name: "category", label: t("Project Category") }, | ||||
@@ -35,6 +35,7 @@ interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | |||||
onClick: (item: T) => void; | onClick: (item: T) => void; | ||||
buttonIcon: React.ReactNode; | buttonIcon: React.ReactNode; | ||||
disabled?: boolean; | disabled?: boolean; | ||||
disabledRows?: { [columnName in keyof T]: string[] }; // Filter the row which is going to be disabled | |||||
} | } | ||||
export type Column<T extends ResultWithId> = | export type Column<T extends ResultWithId> = | ||||
@@ -84,6 +85,22 @@ function SearchResults<T extends ResultWithId>({ | |||||
setPage(0); | 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 = ( | const table = ( | ||||
<> | <> | ||||
<TableContainer sx={{ maxHeight: 440 }}> | <TableContainer sx={{ maxHeight: 440 }}> | ||||
@@ -112,7 +129,7 @@ function SearchResults<T extends ResultWithId>({ | |||||
<IconButton | <IconButton | ||||
color={column.color ?? "primary"} | color={column.color ?? "primary"} | ||||
onClick={() => column.onClick(item)} | onClick={() => column.onClick(item)} | ||||
disabled={Boolean(column.disabled)} | |||||
disabled={Boolean(column.disabled) || Boolean(disabledRows(column, item))} | |||||
> | > | ||||
{column.buttonIcon} | {column.buttonIcon} | ||||
</IconButton> | </IconButton> | ||||